Table of contents
生命不息折腾不止,为了让我的英语小助手与iCloud同步起来,我使用了IceCream来进行Realm本地数据库与远程iCloud同步,之前留下过一张小纸条:通过IceCream让Realm与CloudKit同步小纸条。在改造过程中,为了节约UI主线程上的资源,,所以开始启用Realm的async write,这就给了我一个难题,如何对异步回调进行自动化测试呢?这样的测试不止是async write,还有非async时的各种异步回调,然后经过肘子提示研究了一下XCTestExpectation,记下这张小纸条。
苹果官方有关异步测试也有一个文章:Asynchronous Tests and Expectations ,说明了XCTestExpectation适用的场景:
- Objective-C
- An asynchronous block in a dispatch queue
- A delegate method
- An asynchronous callback, closure, or completion block
- A Future or Promise in Swift Combine
- A situation where it needs to complete within a specific amount of time
简单测试
这是一个完整的简单测试代码:
func test_giveWord_whenWordAsyncDeleteOnceAboutCount(){
if let localRealm = realmController.localRealm?.thaw(){
XCTAssertEqual(localRealm.objects(Word.self).count, 250)
}
let words = realmController.localRealm!.objects(Word.self)
let expectation = XCTestExpectation(description: "delete called")
words[0].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 249)
print("deleted 1: \(count)")
expectation.fulfill()
}
print("deleted: \(self.realmController.localRealm!.objects(Word.self).count)")
wait(for: [expectation], timeout: 2.0)
}
这里首先需要一个XCTestExpectation的实例,我们给它一个说明delete called,然后在回调发生时执行一次fulfill()
,最后再使用wait(for: [expectation], timeout: 2.0)
来用2秒等待它产生预期效果。执行这个Test后,会得到这样的提示:
deleted: 250
deleted 1: 249
如果我们将fulfill()
去除,再执行这个测试,就会得到一个Test Failed的提示:
在log里也会看到这样的信息:
-[CommomLibraryTests.PictureTests test_giveWord_whenWordAsyncDeleteOnceAboutCount] : Asynchronous wait failed: Exceeded timeout of 2 seconds, with unfulfilled expectations: "delete called".
Test Case '-[CommomLibraryTests.PictureTests test_giveWord_whenWordAsyncDeleteOnceAboutCount]' failed (2.277 seconds).
Test Suite 'PictureTests' failed at 2022-05-26 07:41:10.038.
Executed 1 test, with 1 failure (0 unexpected) in 2.277 (2.277) seconds
Test Suite 'CommomLibraryTests.xctest' failed at 2022-05-26 07:41:10.038.
Executed 1 test, with 1 failure (0 unexpected) in 2.277 (2.277) seconds
Test Suite 'Selected tests' failed at 2022-05-26 07:41:10.038.
Executed 1 test, with 1 failure (0 unexpected) in 2.277 (2.278) seconds
Program ended with exit code: 1
测试多次异步调用
如果你在一个测试中需要多次调用,比如会对相同的word执行两次删除,再删除更多个word的测试,这是我的测试代码:
func test_giveWord_whenWordAsyncDeleteFourTimesAboutCount(){
if let localRealm = realmController.localRealm?.thaw(){
XCTAssertEqual(localRealm.objects(Word.self).count, 250)
}
let words = realmController.localRealm!.objects(Word.self)
let expectation = XCTestExpectation(description: "delete called 4 times")
expectation.expectedFulfillmentCount = 4
words[0].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 249)
print("deleted 1: \(count)")
expectation.fulfill()
}
words[0].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 249)
print("deleted 2: \(count)")
expectation.fulfill()
}
words[1].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 248)
print("deleted 3: \(count)")
expectation.fulfill()
}
words[2].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 247)
print("deleted 4: \(count)")
expectation.fulfill()
}
print("deleted: \(self.realmController.localRealm!.objects(Word.self).count)")
wait(for: [expectation], timeout: 2.0)
}
与上个测试不同的地方在于,我加入了
expectation.expectedFulfillmentCount = 4
这样就是告诉XCTestExpectation需要有4次fulfill发生。这是执行结果:
deleted: 250
deleted 1: 249
deleted 2: 249
deleted 3: 248
deleted 4: 247
从显示的顺序和结果我们就可以得出结论,执行完成数据库里还有250条记录,如果我们对一个word进行两次async write的delete操作,并不会发生error,数据库的记录也只会减少一条。所有的删除事务都是依次在Realm的队列中执行的。同样,如果我们把expectedFulfillmentCount
设置为5,再执行测试会得到Failed:
测试异步调用的返回顺序
有时我们需要对异步回调的返回顺序有要求,XCTestExpectation在这方面也做了对应的支持:
func test_giveWord_whenWordAsyncDeleteFourTimesAboutCount(){
if let localRealm = realmController.localRealm?.thaw(){
XCTAssertEqual(localRealm.objects(Word.self).count, 250)
}
let words = realmController.localRealm!.objects(Word.self)
let expectation1 = XCTestExpectation(description: "delete 1 called 2 times")
expectation1.expectedFulfillmentCount = 2
let expectation2 = XCTestExpectation(description: "delete 2 called")
let expectation3 = XCTestExpectation(description: "delete 3 called")
words[0].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 249)
print("deleted 1/1: \(count)")
expectation1.fulfill()
}
words[0].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 249)
print("deleted 1/2: \(count)")
expectation1.fulfill()
}
words[1].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 248)
print("deleted 2: \(count)")
expectation2.fulfill()
}
words[2].delete(isAsync: true) { _ in
let count = self.realmController.localRealm!.objects(Word.self).count
XCTAssertEqual(count, 247)
print("deleted 3: \(count)")
expectation3.fulfill()
}
print("deleted: \(self.realmController.localRealm!.objects(Word.self).count)")
wait(
for: [expectation1,expectation2,expectation3],
timeout: 2.0,
enforceOrder: true
)
}
XCTestExpectation通过在wait中的for数组提供了一个顺序,期待你以这个顺序来调用fulfill()
,为了对顺序进行检查,wait还特别提供了一个参数:enforceOrder: true
。我们来看看如果顺序不对时会是什么样,首先会在fulfill()
处告诉你回调的顺序应该是另一个XCTestExpectation:
另外在wait处也会告诉你顺序不对: