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

·

3 min read

生命不息折腾不止,为了让我的英语小助手与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的提示:

image.png

在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

测试异步调用的返回顺序

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

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

image.png

Did you find this article valuable?

Support 老房东 by becoming a sponsor. Any amount is appreciated!