iOS单元测试-各种问题

编写用例时注意

1.用例只针对 SDK 对外开放的入口

2.注意异常情况和边界值

3.注意用例之间的依赖情况

单测用例如何按照顺序执行

测试函数的执行顺序与测试的字符大小有关系,如- (void)test001Example > - (void)test002Example > - (void)testExample

理论上单元测试不应该控制用例的执行顺序,每个 case 都能够单独执行,但是针对某些特殊的情况,可以适当使用一次此特性。

用例依赖的情况如何处理

当你的用例需要按照顺序执行就说明你的用例存在依赖,针对这种情况就需要进行拆分,放到不同的组(测试文件)当中来执行,比如说存在 a,b,c 三个用例,b 和 c 都依赖于 a,那么比较合理的做法就是将 a 提到另外一个文件当中进行测试,b 和 c 放到另外一个文件中,在 b 和 c 的文件中将 a 放入 setUp 方法用作 b、c 执行的前提,这样就能比较好的进行测试,也减少了大量的冗余代码。

multiple calls made to -[XCTestExpectation fulfill]

出现这种情况是因为写在回调当中的 XCTestExpectation 被多次调用导致的,解决这个错误,网上目前有以下三种解决方案,可以根据自己的情况进行选择:

1.使用 __weak 修饰 XCTestExpectation。

[expectation fulfill] 执行一次后方法执行完毕,局部变量 expectation 被释放,当回调再次调起时,expectation 已被释放置 nil,执行 fulfill 不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
- (void)testExample {
NSString *username = @"shuhai";
NSString *password = @"123";

__weak XCTestExpectation *expectation = [self expectationWithDescription:@"登录请求返回"];

[SHUserCenter loginWithUserName: username password: password callback:^(NSDictionary *data) {
XCTAssertNotNil([data objectForKey:@"data"],@"返回结果不能为nil");
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:self.timeout handler:nil];
}

但是该种方法存在一个问题,就是当有多个 XCTestExpectation 时,靠前的 expectation 执行 fulfill 时,该方法并不一定结束,导致再次调用 fulfill 报错,如下当 noticeExpectation 所在的回调被第二次调用时,expectation 可能并未 fulfill,变回导致 noticeExpectation 多次 fulfill 报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)testExample {
NSString *username = @"shuhai";
NSString *password = @"123";

__weak XCTestExpectation *noticeExpectation = [self expectationWithDescription:@"登录通知返回"];
[SHUserCenter registerNotice:SHUCenterNoticeTypeLoginSuccess handle:^(NSDictionary *data) {
XCTAssertNotNil([data objectForKey:@"data"],@"返回结果不能为nil");
[noticeExpectation fulfill];
}];

__weak XCTestExpectation *expectation = [self expectationWithDescription:@"登录请求返回"];
[SHUserCenter loginWithUserName: username password: password callback:^(NSDictionary *data) {
XCTAssertNotNil([data objectForKey:@"data"],@"返回结果不能为nil");
[expectation fulfill];
}];
[self waitForExpectations:@[noticeExpectation,expectation] timeout:self.timeout];
}

2.使用 __block 修饰 XCTestExpectation 并在 fulfill 之后置为 nil

当 XCTestExpectation 对象执行过 fulfill 之后被置为了 nil,再次调用 fulfill 则不会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)testExample {
NSString *username = @"shuhai";
NSString *password = @"123";

__block XCTestExpectation *expectation = [self expectationWithDescription:@"登录请求返回"];

[SHUserCenter loginWithUserName: username password: password callback:^(NSDictionary *data) {
XCTAssertNotNil([data objectForKey:@"data"],@"返回结果不能为nil");
[expectation fulfill];
expectation = nil;
}];
[self waitForExpectationsWithTimeout:self.timeout handler:nil];
}

这种方法在大部分情况可以可以解决问题,有种特殊的情况我们后面再讨论

3.移除回调的多次调用逻辑。

查找并移除回调多次调用的逻辑是从根本上解决这种问题的方法,但是某些业务场景下我们可能是需要回调多次调用的,针对这种情况,我们就可以采用第二种方法来解决问题。

超时之后 fulfill 错误无法上报

这种错误是因为等待 XCTestExpectation 超时测试方法执行完毕后,回调调用(比如网络不好超时的情况),而且返回了一个错误的结果,在回调当中的断言失败却无法上报错误导致的崩溃,错误信息大致如下:

1
2
3
4
5
6
Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Unable to report test assertion failure '((data) != nil)
failed: throwing "Unable to report test assertion failure '((data) != nil) failed' from /Users/yzq/Documents/Component/CHLUserCenter/Example/Tests/CHLUserCenterNoDataTests.m:31 because it was raised inside test case -[CHLUserCenterNoDataTests test001] which has no associated XCTestRun object.
This may happen when test cases are constructed and invoked independently of standard XCTest infrastructure, or when the test has already finished."
' from /Users/yzq/Documents/Component/CHLUserCenter/Example/Tests/CHLUserCenterNoDataTests.m:31 because it was raised inside test case -[CHLUserCenterNoDataTests test001] which has no associated XCTestRun object.
This may happen when test cases are constructed and invoked independently of standard XCTest infrastructure, or when the test has already finished.'

这种错误在上面提到的使用 __block 的情况下也不能幸免,解决方案就是使用一个 __block 修饰的变量,将回调当中要获取的值取出来,然后再外层进行数据判断,这样就不会导致断言在回调当中崩溃了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)testExample {
NSString *username = @"shuhai";
NSString *password = @"123";

__block XCTestExpectation *expectation = [self expectationWithDescription:@"登录请求返回"];
__block NSDictionary *returnData = nil;

[SHUserCenter loginWithUserName: username password: password callback:^(NSDictionary *data) {
returnData = data;
[expectation fulfill];
expectation = nil;
}];
[self waitForExpectationsWithTimeout:self.timeout handler:nil];
XCTAssertNotNil([returnData objectForKey:@"data"],@"返回结果不能为nil");
}

Assertion failure in -[XCTestExpectation fulfill]

这个错误是当有多个 XCTestExpectation 时使用 waitForExpectations:timeout: 等待 Expectation 导致,导致错误的具体原因目前还不是很明确,有了解具体原因的同学可以告诉我下。

针对这个问题,我们可以使用 waitForExpectationsWithTimeout:handler: 来等待 Expectation,这样就不会报错了。

attempt to insert nil object from objects[0]

这个错误是因为我们在前面使用 __block 修饰 XCTestExpectation 并置为 nil 操作的解决方案时同时使用了 waitForExpectations:timeout: 来等待 Expectation,向数组中插入了 nil。

这个问题的解决方案同上,使用 waitForExpectationsWithTimeout:handler: 来替换即可。所以针对大多数啊情况推荐使用 waitForExpectationsWithTimeout:handler: 来等待 XCTestExpectation。