【问题标题】:How can I get XCTest to wait for async calls in setUp before tests are run?如何让 XCTest 在运行测试之前等待 setUp 中的异步调用?
【发布时间】:2015-06-12 19:47:19
【问题描述】:

我正在 Xcode 6 中编写集成测试,以配合我的单元和功能测试。 XCTest 有一个 setUp() 方法,在每次测试之前都会被调用。伟大的!

它还有 XCTestException 让我可以编写异步测试。也很棒!

但是,我想在每次测试之前用测试数据填充我的测试数据库,并且 setUp 只是在异步数据库调用完成之前开始执行测试。

有没有办法让 setUp 等到我的数据库准备好后再运行测试?

这是我现在所做的一个例子。由于 setUp 在数据库完成填充之前返回,因此每次测试都必须复制大量测试代码:

func test_checkSomethingExists() {

    let expectation = expectationWithDescription("")
    var expected:DatabaseItem

    // Fill out a database with data. 
    var data = getData()
    overwriteDatabase(data, {
      // Database populated.
      // Do test... in this pseudocode I just check something...
      db.retrieveDatabaseItem({ expected in

        XCTAssertNotNil(expected)

        expectation.fulfill()
      })
    })

    waitForExpectationsWithTimeout(5.0) { (error) in
        if error != nil {
            XCTFail(error.localizedDescription)
        }
    }

}

这是我想要的:

class MyTestCase: XCTestCase {

    override func setUp() {
        super.setUp()

        // Fill out a database with data. I can make this call do anything, here
        // it returns a block.
        var data = getData()
        db.overwriteDatabase(data, onDone: () -> () {

           // When database done, do something that causes setUp to end 
           // and start running tests

        })        
    }

    func test_checkSomethingExists() {

        let expectation = expectationWithDescription("")
        var expected:DatabaseItem


          // Do test... in this pseudocode I just check something...
          db.retrieveDatabaseItem({ expected in

            XCTAssertNotNil(expected)

            expectation.fulfill()
        })

        waitForExpectationsWithTimeout(5.0) { (error) in
            if error != nil {
                XCTFail(error.localizedDescription)
            }
        }

    }

}

【问题讨论】:

  • 如果你在堆栈溢出中搜索“[ios] 异步单元测试”,你会看到大量的答案,不仅有XCTestExpectation(不是XCTestException)技术,还有信号量技术.例如stackoverflow.com/a/23658385/1271826。您可能可以将信号量技术用于您的异步数据库代码(尽管您还没有分享您是如何处理这个数据库的,所以我们不能比这更具体)。我很惊讶您的数据库没有同步功能,因为这在数据库中很常见。
  • Rob,我编辑了我的问题以准确显示我在寻找什么。我确实知道如何使用 XCTest 和 XCTestException 来编写异步测试。我不知道如何在设置完成之前阻止测试运行。谢谢。
  • 大声笑。同样,没有XCTestException 这样的东西。这是XCTestExpectation。正如我所说,在setUp 中使用信号量技术,而不是XCTestExpectation。 (在测试中使用期望值,但在 setUp 中使用信号量。)
  • Re: XCTestException --- 代码阅读障碍再次来袭!大声笑
  • 是否可以将您的数据库设置代码放在辅助方法中?那么你每次测试只有一个重复的行。

标签: xcode unit-testing swift asynchronous xctest


【解决方案1】:

运行异步测试有两种技术。 XCTestExpectation 和信号量。如果在setUp 中做一些异步操作,你应该使用信号量技术:

override func setUp() {
    super.setUp()

    // Fill out a database with data. I can make this call do anything, here
    // it returns a block.

    let data = getData()

    let semaphore = DispatchSemaphore(value: 0)

    db.overwriteDatabase(data) {

        // do some stuff

        semaphore.signal()
    }

    semaphore.wait()
}

注意,要让它工作,这个onDone 块不能在主线程上运行(否则你会死锁)。


如果这个 onDone 块在主队列上运行,你可以使用运行循环:

override func setUp() {
    super.setUp()

    var finished = false

    // Fill out a database with data. I can make this call do anything, here
    // it returns a block.

    let data = getData()

    db.overwriteDatabase(data) {

        // do some stuff

        finished = true
    }

    while !finished {
        RunLoop.current.run(mode: .default, before: Date.distantFuture)
    }
}

这是一个非常低效的模式,但根据overwriteDatabase 的实现方式,可能有必要

注意,仅当您知道 onDone 块在主线程上运行时才使用此模式(否则您必须对 finished 变量进行一些同步)。

【讨论】:

  • 如果 onDone 块在主线程中运行,有没有办法让这个工作?
  • 我已经更新了这个例子,如果onDone 在主线程上运行,则使用运行循环。
  • 有没有办法在覆盖类 func setUp() { } 内进行异步一次性设置,所有测试用例只应调用一次?
  • @IlkerBaltaci - 我原以为你会在 class rendition 中这样做。见Understanding Setup and Teardown for Test Methods
【解决方案2】:

Swift 4.2

使用这个扩展:

import XCTest

extension XCTestCase {
    func wait(interval: TimeInterval = 0.1 , completion: @escaping (() -> Void)) {
        let exp = expectation(description: "")
        DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
            completion()
            exp.fulfill()
        }
        waitForExpectations(timeout: interval + 0.1) // add 0.1 for sure asyn after called
    }
}

和这样的用法:

func testShoudDeleteSection() {
        let tableView = TableViewSpy()
        sut.tableView = tableView

        sut.sectionDidDelete(at: 0)

        wait {
            XCTAssert(tableView.isReloadDataCalled, "Chcek relaod table view after section delete")
        }
    }

上面的例子并不完整,但你可以理解。希望对您有所帮助。

【讨论】:

    【解决方案3】:

    您可以使用与异步测试用例相同的 waitForExpectationsWithTimeout:handler: 函数,而不是使用信号量或阻塞循环。

    // Swift
    override func setUp() {
        super.setUp()
    
        let exp = expectation(description: "\(#function)\(#line)")
    
        // Issue an async request
        let data = getData()
        db.overwriteDatabase(data) {
            // do some stuff
            exp.fulfill()
        }
    
        // Wait for the async request to complete
        waitForExpectations(timeout: 40, handler: nil)
    }
    
    // Objective-C
    - (void)setUp {
        [super setUp];
    
        NSString *description = [NSString stringWithFormat:@"%s%d", __FUNCTION__, __LINE__];
        XCTestExpectation *exp = [self expectationWithDescription:description];
    
        // Issue an async request
        NSData *data = [self getData];
        [db overwriteDatabaseData: data block: ^(){
            [exp fulfill];
        }];        
    
        // Wait for the async request to complete
        [self waitForExpectationsWithTimeout:40 handler: nil];
    }
    

    【讨论】:

    • 太棒了!这是 Apple 推荐的 - 在 Xcode API 参考中搜索 XCTestExpectation。希望我能加倍投票支持同时包含 ObjC 和 Swift!
    • 这应该是公认的答案。 NSHipster 有一篇很棒的文章解释了如何使用异步测试 nshipster.com/xctestcase
    • 附带说明,当您使用覆盖类 func setup() 时,这不起作用
    猜你喜欢
    • 1970-01-01
    • 2014-10-21
    • 1970-01-01
    • 2020-06-22
    • 1970-01-01
    • 2014-09-01
    • 1970-01-01
    • 2013-06-27
    • 2021-01-07
    相关资源
    最近更新 更多