# 使用XCTestExpectation进行异步测试小纸条

生命不息折腾不止，为了让我的英语小助手与iCloud同步起来，我使用了IceCream来进行Realm本地数据库与远程iCloud同步，之前[留下过一张小纸条:通过IceCream让Realm与CloudKit同步小纸条](https://blog.hdcola.one/icecream-realm-cloudkit)。在改造过程中，为了节约UI主线程上的资源，，所以开始启用Realm的[async write](https://www.mongodb.com/docs/realm/sdk/swift/examples/read-and-write-data/#std-label-ios-async-write)，这就给了我一个难题，如何对异步回调进行自动化测试呢？这样的测试不止是async write，还有非async时的各种异步回调，然后经过肘子提示研究了一下XCTestExpectation，记下这张小纸条。

苹果官方有关异步测试也有一个文章：[Asynchronous Tests and Expectations
](https://developer.apple.com/documentation/xctest/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的提示：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1653565389126/z9FNKt6qX.png align="left")

在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：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1653565934857/I581_8gMD.png align="left")

### 测试异步调用的返回顺序

有时我们需要对异步回调的返回顺序有要求，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：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1653566795867/xVNd2L5c3.png align="left")

另外在wait处也会告诉你顺序不对：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1653566838117/Xv7JRgHm-.png align="left")

