如何让 XCTest 在运行测试之前在 setUp 中等待异步调用?

我正在用 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)
}
}


}


}
31308 次浏览

运行异步测试有两种技术。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块在主队列上运行,可以使用 run 循环:

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变量进行一些同步)。

与使用信号量或阻塞循环不同,您可以使用在异步测试用例中使用的相同 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];
}

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")
}
}

上面的例子不完整,但你可以得到的想法。希望这个帮助。