本文共 17608 字,大约阅读时间需要 58 分钟。
在计算机编程中,单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
通常来说,程序员每修改一次代码就会修改某个单元,那我们就可以对这个单元做修改的验证(单元测试),在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书(产品需求)要求的工作目标,而且没有程序错误。虽然单元测试不是什么必须的,但也不坏,这牵涉到项目管理的政策决定。
单元测试可以方便测试一些功能是否正常运行,调试接口是否能正常使用。有时候你可能是为了测试某一个网络接口,然后每次都重新启动并且经过很多操作之后才测试到了那个网络接口,如果使用了单元测试,就可以直接测试那个方法,相对方便很多。比如由于修改较多,我们想测试一下分享功能是否正常,这时候就有用了,而不是重新启动程序,进入到分享界面,点击分享,填写分享内容。其实单元测试并没有降低我们打代码的效率,我们可以在单元测试通过了,直接用到相应的地方。
当然单元测试也有一些高级的作用,比如自动发布、自动测试(特别在一些大的项目,以防止程序被误改或引起新的问题)。
XCTest 是苹果自带的测试框架。
GHUnit 是一个可视化的测试框架,有了它,你可以点击 APP 来决定测试哪个方法,并且可以点击查看测试结果等。
OCMock 是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法 A,方法 A 里面调用到了方法 B,而且方法 B 是有参数传入,但又不是方法 A 所提供。这时候,你可以使用 OCMock 来模拟方法 B 返回的值。在不影响测试的情况下,就可以这样去模拟。除了这些,在没有网络的情况下,也可以通过 OCMock 模拟返回的数据。
UITests 是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候 UITests 就可以帮助解决这个问题了。
1、单元测试是以代码测试代码。不是靠 NSLog 来测试,NSLog 是程序员用眼睛看的笨办法。而是使用断言来测试的,提前预判条件必须满足。
XCTAssert(expression, ...) XCTAssert(条件, 不满足条件的描述)
2、单元测试与应用程序开发属于共存关系,而非嵌入关系,所以必须创建一个单独的测试目标。
3、可以在单元测试类中编写单独的测试用例方法。这些方法与普通的方法类似,但是方法名称必须以 test 开头,且不能有参数,不然不会识别为测试方法。
4、测试方法可以直接写在 - (void)testExample
中,或者写在以 test 开头的测试用例方法中。
5、单元测试需要在真机上进行,为了能够在设备中真实地运行应用程序用例,需要安装开发配置文件(development provision file)。
6、需要注意,在应用程序上运行单元测试用例并不是一个交互过程,所有的运行控制(包括提供值)都由测试用例自身掌握。
7、不是所有的方法都需要测试。例如私有方法不需要测试,只有暴露在 .h 中的方法需要测试。
8、一般而言,代码的覆盖度大概在 50% ~ 70%。从 github 上得知:YYModel 测试覆盖度为 83%,AFNetworking 测试覆盖度为 77%,两者都是比较高的。
单元调试操作,两种方法,按快捷键 Command + U 进行单元测试,这个快捷键是全部测试。
调试可以在断言处调试,也可以在函数部分调试。错误提示是在断言处显示,不会在平台展示。
测试方法
- (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. // 初始化的代码,在测试方法调用之前调用 } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. // 释放测试用例的资源代码,这个方法会每个测试用例执行后调用 [super tearDown]; } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. // 测试用例的方法 } - (void)testPerformanceExample { // This is an example of a performance test case. // 测试性能的方法,有 Instrument 调试工具之后,其实这个没毛用 [self measureBlock:^{ // Put the code you want to measure the time of here. // 需要测试性能的代码 }]; }
测试函数
// 生成一个失败的测试 XCTFail(format…); // 为空判断 // 为空判断,a1 为空时通过,反之不通过 XCTAssertNil(a1, format...); // 不为空判断,a1 不为空时通过,反之不通过 XCTAssertNotNil(a1, format…); // 为真判断 // 为真判断,当 expression 求值为 True 时通过 XCTAssert(expression, format...); // 为真判断,当 expression 求值为 True 时通过 XCTAssertTrue(expression, format...); // 为假判断,当 expression 求值为 False 时通过 XCTAssertFalse(expression, format...); // 相等判断 // 相等判断,[a1 isEqual:a2] 值为 True 时通过,其中一个不为空时,不通过 XCTAssertEqualObjects(a1, a2, format...); // 不等判断,[a1 isEqual:a2] 值为 False 时通过 XCTAssertNotEqualObjects(a1, a2, format...); // 相等判断,当 a1 和 a2 是 C 语言标量、结构体或联合体时使用,a1 == a2 值为 True 时通过 XCTAssertEqual(a1, a2, format...); // 不等判断,当 a1 和 a2 是 C 语言标量、结构体或联合体时使用 XCTAssertNotEqual(a1, a2, format...); // 相等判断,double 或 float 类型,提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试 XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...); // 不等判断,double 或 float类型,提供一个误差范围,当在误差范围以内不等时通过测试 XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...); // 异常判断 // 异常判断,当 expression 发生异常时通过,反之不通过 XCTAssertThrows(expression, format...); // 异常判断,当 expression 发生 specificException 异常时通过,反之发生其他异常或不发生异常均不通过 XCTAssertThrowsSpecific(expression, specificException, format...); // 异常判断,当 expression 发生具体异常、具体异常名称的异常时通过测试,反之不通过 XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...); // 异常判断,当 expression 没有发生异常时通过测试 XCTAssertNoThrow(expression, format…); // 异常判断,当 expression 没有发生具体异常、具体异常名称的异常时通过测试,反之不通过 XCTAssertNoThrowSpecific(expression, specificException, format...); // 异常判断,当 expression 没有发生具体异常、具体异常名称的异常时通过测试,反之不通过 XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...);
基本使用
- (void)testExample { NSLog(@"自定义测试 testExample"); int a = 3; XCTAssertTrue(a == 0, "a 不能等于 0"); }
点击播放按钮,开始单个方法的测试
出现如下结果,由于我们断言 a 是等于 0 的,而 a 等于 3,所以测试没有通过。
问题描述:fatal error: 'XCTest/XCTest.h' file not found
解决方法
在报错的 Target 中的 Building settings 中 FRAMEWORK_SEARCH_PATHS* 添加 $(PLATFORM_DIR)/Developer/Library/Frameworks
单例要在并发条件下测试
// 测试是否为单例 - (void)testAudioManagerSingle { // 要在并发条件下测试 NSMutableArray *managers = [NSMutableArray array]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ QAudioManager *tempManager = [[QAudioManager alloc] init]; [managers addObject:tempManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ QAudioManager *tempManager = [[QAudioManager alloc] init]; [managers addObject:tempManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ QAudioManager *tempManager = [QAudioManager defaultManager]; [managers addObject:tempManager]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ QAudioManager *tempManager = [QAudioManager defaultManager]; [managers addObject:tempManager]; }); QAudioManager *managerOne = [QAudioManager defaultManager]; // 这里是判断数组中的对象是否一致 [managers enumerateObjectsUsingBlock:^(QAudioManager *obj, NSUInteger idx, BOOL * _Nonnull stop) { XCTAssertEqualObjects(managerOne, obj, @"QAudioManager is single"); XCTAssertNotEqualObjects(managerOne, obj, @"QAudioManager is not single"); }]; }
性能测试
+ (instancetype)personWithDict:(NSDictionary *)dic { NSString *str1; for (NSString *str in dic) { str1 = [str stringByAppendingString:str]; } str1 = nil; Person *one = [[self alloc] init]; return one; } - (void)testPerformanceExample { // This is an example of a performance test case. // 测试性能的方法,有 Instrument 调试工具之后,其实这个没毛用 [self measureBlock:^{ // Put the code you want to measure the time of here. // 需要测试性能的代码 NSTimeInterval start = CACurrentMediaTime(); // 测试用例,循环10000次,为了演示效果 for (NSInteger i = 0; i < 10000; i++) { [Person personWithDict:@{@"name":@"zhang", @"age":@20}]; } // 传统测试代码耗时方法 NSLog(@"%lf, 我是香蕉大大", CACurrentMediaTime() - start); }]; }
逻辑测试
// 逻辑测试 - (void)testNewPerson { // 1.测试 name 和 age 是否一致 [self checkPersonWithDict:@{@"name":@"zhou", @"age":@30}]; /** 2.测试出 age 不符合实际,那么需要在字典转模型方法中对 age 加以判断: if (obj.age <= 0 || obj.age >= 130) { obj.age = 0; } */ [self checkPersonWithDict:@{@"name":@"zhang", @"age":@200}]; // 3.测试出 name 为 nil 的情况,因此在 XCTAssert 里添加条件:“person.name == nil“ [self checkPersonWithDict:@{}]; // 4.测试出 Person 类中没有 title 这个 key,在字典转模型方法中实现:- (void)setValue:(id)value forUndefinedKey:(NSString *)key {} [self checkPersonWithDict:@{@"name":@"zhou", @"age":@30, @"title":@"boss"}]; // 5.总体再验证一遍,结果 Build Succeeded,测试全部通过 [self checkPersonWithDict:@{@"name":@"zhou", @"age":@-1, @"title":@"boss"}]; } // 根据字典检查新建的 person 信息 - (void)checkPersonWithDict:(NSDictionary *)dict { Person *person = [Person personWithDict:dict]; NSLog(@"%@",person); // 获取字典中的信息 NSString *name = dict[@"name"]; NSInteger age = [dict[@"age"] integerValue]; // 1.检查名字 XCTAssert([name isEqualToString:person.name] || person.name == nil, @"姓名不一致"); // 2.检查年龄 if (person.age.integerValue > 0 && person.age.integerValue < 130) { XCTAssert(age == person.age.integerValue, @"年龄不一致"); } else { XCTAssert(person.age == 0, @"年龄超限"); } }
安装 AFNetworking 和 STAlertView
由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。在方法结束前设置等待,调回回来的时候再让它继续执行。
//waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。 #define WAIT do {\ [self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\ [self waitForExpectationsWithTimeout:30 handler:nil];\ } while (0); #define NOTIFY \ [[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil];
增加测试方法 testRequest
- (void)testRequest{ // 获得请求管理者 AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager]; mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil]; // 发送 GET 请求 [mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { XCTAssertNotNil(responseObject, @"返回出错"); NOTIFY // 继续执行 } failure:^(AFHTTPRequestOperation *operation, NSError *error) { XCTAssertNil(error, @"请求出错"); NOTIFY // 继续执行 }]; WAIT //暂停 }
有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框 STAlertView,前面已经设置。
self.stAlertView = [[STAlertView alloc] initWithTitle:@"验证码" message:nil textFieldHint:@"请输入手机验证码" textFieldValue:nil cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancelButtonBlock:^{ // 点击取消返回后执行 [self testAlertViewCancel]; NOTIFY // 继续执行 } otherButtonBlock:^(NSString *b) { // 点击确定后执行 [self alertViewComfirm:b]; NOTIFY // 继续执行 }]; [self.stAlertView show];
高级自动化单元测试,推荐看 LeanCloud 工程师的,自动化单元测试,自动化发布都有讲到。
李智维的演示 。
Mock 测试
Mock 测试是个很神奇而又很酷的技术,在测试过程中,对于一些不容易构造或不容易获取的对象,此时你可以创建一个虚拟的对象(mock object)来完成测试。
例如你可能要尝试 100 次才会返回一个 NSError,通过 mock object 你可以自行创建一个 NSError 对象,测试在出错情况下程序的处理是否符合你的预期。
例如你要连接服务器但是服务器在实验室,你在外工作的时候就无法测试了,这个时候你可以创建一个虚拟的服务器,并返回一些你指定的数据,从而绕过服务器。
例如假设你要访问一个数据库,但是访问过程的开销巨大,这时你可以虚拟一个数据库,并且返回一些自行定制的数据,从而绕过了数据库的访问。
Mock 的思想很简单:没有条件?我们就自行创造条件。
OCMock
OCMock 是一个用于为 iOS 或 macOS 项目配置 Mock 测试的开源项目,如果目标是 iOS 项目那么生成的是静态库,如果是 macOS 项目生成的是框架。OCMock 其实现思想就是根据要 mock 的对象的 class 来创建一个对应的对象,并且设置好该对象的属性和调用预定方法后的动作(例如返回一个值,调用代码块,发送消息等等),然后将其记录到一个数组中,接下来开发者主动调用该方法,最后做一个 verify(验证),从而判断该方法是否被调用,或者调用过程中是否抛出异常等。
:在 iOS 项目中配置 OCMock 的教程
:在 GitHub 上的示例项目,可以参考下其中的一些配置参数
:OCMock 的静态库、框架和工程文件(可以在这里看 OCMock 的源码实现)下载地址,已经打包成 dmg 格式了。
1、下载 OCMock Download 的 dmg 文件,将 iOS library 文件夹中的文件(libOCMock.a 和 OCMock 文件夹)拷贝到要测试的项目根目录下。打开工程,将拷贝的文件添加到项目工程中。
2、打开 OCMockDemoTests Target 的 Build Phases,添加 libOCMock.a 到要链接的类库中。
3、打开 Build Settings,搜索 Other Linker Flags,设置如下
-force_load "$(SRCROOT)/OCMock/libOCMock.a" -ObjC
4、再搜索 Header Search Paths,设置如下
"$(SRCROOT)/OCMock"
新建一个 test case class 类,基类为 XCTestCase,命名为 MockTableTests。
首先我们测试一下 TableDataSource 的 numberOfRowsInSection 方法是否返回了正确的值,测试代码如下
- (void)testNumberOfRows { // 创建 Table View 的 DataSource TableViewCellConfigureBlock cellConfigureBlock = ^(UITableViewCell *cell, NSString *item) { cell.textLabel.text = item; }; TableDataSource *tableSource = [[TableDataSource alloc] initWithItems:@[@"1", @"2", @"3"] CellIdentifier:@"foo" ConfigureCellBlock:cellConfigureBlock]; // 创建 mock table view id mockTableView = [OCMockObject mockForClass:[UITableView class]]; // 断言 XCTAssertEqual([tableSource tableView:mockTableView numberOfRowsInSection:0], (NSInteger)3, @"Mock table returns a bad number of rows in section 0"); }
1、首先创建 data source,用于下文中调用 numberOfRowsInSection 方法。注意这里 Table View 中的内容 @[@"1", @"2", @"3"] 是需要我们手动配置的。这里也体现了 mock 的一个局限性,就是 mock object 的关键属性都要我们自己定制,如果要模拟的对象非常的大,那么创建一个 mock object 的成本将远远大于单元测试带来的效益。
2、如果要单独测试 numberOfRowsInSection 方法,我们就需要有一个 TableView,因此要通过 OCMockObject 的 mockForClass 类方法来创建一个 mock table view。
3、通过 data source 调用方法,并使用断言判断。
如果在测试时,我们只想在控制台中看见这个方法的输出信息,可以点击方法前面的一个小播放按钮
控制台输出
下面来编写一个稍微复杂点的 mock 测试,用来测试 UITableViewDataSource 中的 cellForRowAtIndexPath 方法。
- (void)testCellConfiguration { // 创建 Table data source __block UITableViewCell *configuredCell = nil; __block id configuredObject = nil; TableViewCellConfigureBlock block = ^(UITableViewCell *a, id b) { configuredCell = a; configuredObject = b; }; TableDataSource *dataSource = [[TableDataSource alloc] initWithItems:@[@"a", @"b"] CellIdentifier:@"foo" ConfigureCellBlock:block]; // 创建 mock table view id mockTableView = [OCMockObject mockForClass:[UITableView class]]; // 设定 mock table view 的行为 UITableViewCell *cell = [[UITableViewCell alloc] init]; [[[mockTableView expect] andReturn:cell] dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; // [[[mockTableView stub] andReturn:cell] dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; // 主动调用 cellForRowAtIndexPath 方法 id result = [dataSource tableView:mockTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; // 验证 mock table view 的行为 [mockTableView verify]; // 断言 XCTAssertEqual(result, cell, @"Should return the dummy cell."); XCTAssertEqual(configuredCell, cell, @"This should have been passed to the block."); XCTAssertEqualObjects(configuredObject, @"a", @"This should have been passed to the block."); }
1、创建 Table data source,用于下文调用 cellForRowAtIndexPath 方法。
2、创建 mock table view。
3、如果 mock table view 调用了 dequeueReusableCellWithIdentifier:@"foo" forIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]
方法,那么就返回上面已经创建好的 UITableViewCell 对象,expect 方法表示该方法必须被调用(见5.)。
4、通过 Table data source 主动调用 cellForRowAtIndexPath 方法,此时会触发 mock table view 调用 dequeueReusableCellWithIdentifier:forIndexPath:
方法。
5、最后要调用 verify 方法,用于验证 mock table view 的行为。如果 mock table view 在某个方法中调用了 expect,那么该方法必须在 verify 之前被调用,否则测试无法通过。如果 mock table view 调用的是 stub,那么 verify 时 OCMock 并不关心该方法是否调用过,只会关心调用过程是否发生异常或有测试被拒绝等。
6、断言,在这里进行各种比较。
可能大家都注意到了,在运行测试后,控制台中的输出可以用惨不忍睹来形容。这时我们可以尝试另一个工具:GHUnit 框架,这个工具是有 GUI 的。
:该项目在 GitHub 上的地址。
:编写测试的参考文档。
UITests 是一个自动测试 UI 与交互的 Testing 组件。它可以通过编写代码、或者是记录开发者的操作过程并代码化,来实现自动点击某个按钮、视图,或者自动输入文字等功能。
在实际的开发过程中,随着项目越做越大,功能越来越多,仅仅靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候 UITests 就可以帮助解决这个问题了。
1、XCUIApplication
继承 XCUIElement,这个类掌管应用程序的生命周期,里面包含两个主要方法
launch():启动程序
terminate():终止程序
2、XCUIElement
继承 NSObject,实现协议 XCUIElementAttributes, XCUIElementTypeQueryProvider
可以表示系统的各种UI元素
3、exist
4、descendantsMatchingType(type:XCUIElementType)->XCUIElementQuery:
5、childrenMatchingType(type:XCUIElementType)->XCUIElementQuery:
取某种类型的元素集合,不包含它的子类
这两个方法的区别在于,你仅使用系统的 UIButton 时,用 childrenMatchingType 就可以了,如果你还希望查询自己定义的子 Button,就要用 descendantsMatchingType
6、另外 UI 元素还有一些交互方法
7、XCUIElementAttributes 协议
里面包含了 UIAccessibility 中的部分属性
可以方便你查看当前元素的特征,其中 identifier 属性可用于直接读取元素,不过该属性在 UITextField 中有 bug,暂时不清楚原因
8、XCUIElementTypeQueryProvider 协议
里面包含了系统中大部分 UI 控件的类型,可通过读属性的方式取得某种类型的 UI 集合,部分属性截图如下
1、如果是新项目,则创建工程的时候可以直接勾选选项,如下图
2、如果是已有的项目,可以通过添加 target 的方式添加一个 UI Tests,点击 xcode 的菜单,找到 target 栏
在 Test 选项中选择 Cocoa Touch UI Testing Bundle
3、这时候 test 组件添加成功,它在项目中的位置如下图所示
1、手动创建测试代码
打开测试文件,在 testExample() 方法中添加测试代码,如果不知道如何写测试代码,则可以参考自动生成的代码样式。
2、自动生成测试步骤
选择测试文件后,点击录制按钮。
这时候开始进行操作,它会记录你的操作步骤,并生成测试代码,下图就是在一些操作后自动生成的测试代码。
这时候可以分析测试代码的语法,以便你自己手动修改或者手写测试代码。
3、开始测试
点击 testExample 方法旁边的播放按钮,它就开始进行自动测试了,这时候你会看到 App 在自动操作。
转载地址:http://kjpdx.baihongyu.com/