Skip to main content

Command Palette

Search for a command to run...

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

Published
3 min read
使用XCTestExpectation进行异步测试小纸条

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

More from this blog

Https 本地服务器小纸条

最近想要尝试一下Telegram mini app,在调试时需要使用https的服务。试来试去发现在Mac下完成一个简单的HTTPS服务器还是需要点奇奇怪怪的工具的。但是整体来讲非常简单。 准备证书 主要会使用mkcert来创建证书。首先安装mkcert。 brew install mkcert brew install nss # if use Firefox 将mkcert加入到本地root CA。 mkcert -install 生成证书 本地调试可以使用localhost或127.0...

Feb 21, 20241 min read
Https 本地服务器小纸条

macOS中使用Docker发布一个python项目的小纸条

最近写了一个Telegram Bot,它可以使用语音和文字与GPT进行交互,成为了我日常重度使用的工具。从练习英语的听说读,到日常的搜索使用上都让我有了不少收获。终于,日常跑在我笔记本上的日子就要过去了,我需要它能日常跑在我的服务器上,所以准备使用Docker整个image,使得我日常的更新和服务器的迁移更为简单些。所以写下这个小纸条,方便以后自己回来查看。 安装Docker 使用Homebrew安装简单方便: brew install --cask docker Homebrew会视你的机器...

Mar 10, 20233 min read
macOS中使用Docker发布一个python项目的小纸条

老房东的纸条箱

39 posts