我尝试熟悉TDD。如何测试异步的URLSession调用?哪个XCAssert更好用,哪个阶段?
我的第一个想法是创建一个函数,其中包含URLSession,并在此函数内将bool标志设置为true,然后在XCAssertTrue中对其进行测试。或者另一个想法是在调用包含USLSession代码的函数后同步返回伪数据。
答案 0 :(得分:3)
好问题。我认为它可以分为两部分,第一部分是关于如何使用XCTest
测试异步代码,第二部分是关于你建议的策略。
异步代码的问题在于它作为测试在不同的线程上运行,这导致测试在异步调用之后继续,并在完成之前完成。
XCTest框架提供了仅为此用例构建的waitForExpectationsWithTimeout:handler
函数。它允许您等待异步调用完成,然后才检查其结果。
您可以使用它:
import XCTest
@testable import MyApp
class CallbackTest: XCTestCase {
func testAsyncCalback() {
let service = SomeService()
// 1. Define an expectation
let expectation = expectationWithDescription("SomeService does stuff and runs the callback closure")
// 2. Exercise the asynchronous code
service.doSomethingAsync { success in
XCTAssertTrue(success)
// Don't forget to fulfill the expectation in the async callback
expectation.fulfill()
}
// 3. Wait for the expectation to be fulfilled
waitForExpectationsWithTimeout(1) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
}
您可以在this blog post中详细了解该方法。 完全披露,我写了。
现在,关于你的建议:
我的第一个想法是创建一个函数,其中包含URLSession,并在此函数内将bool标志设置为true,然后在
XCAssertTrue
中对其进行测试。
好主意。这是测试是否实际调用异步回调的好方法。唯一要记住的是测试需要等待异步调用运行,否则它将始终失败。
使用上面的技巧你可以写:
let service = SomeService()
var called = false
let expectation = expectationWithDescription(
"The callback passed to SomeService is actually called"
)
service.doSomethingAsync { _ in
called = true
expectation.fulfill()
}
waitForExpectationsWithTimeout(1) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
XCTAssertTrue(called)
}
或者另一个想法是在调用包含
URLSession
代码的函数后同步返回伪数据。
这是另一个好主意。在仅执行一项操作的组件中分解软件,完全隔离测试它们,最后测试它们是否与一些强调快乐和最常见故障路径的集成测试一起工作。
由于您提到URLSession
,我想留下关于触及网络的单元测试代码的警告说明。在单元测试中进行真正的网络调用通常是一个坏主意。事实上,它将测试绑定到网络可用并返回预期结果的事实,并使它们变慢。
在单位级别,我们希望我们的测试是孤立的,确定的和快速的(更多关于此here)。击中网络并不符合这些目标。
避免攻击网络的一个好方法是将其存根,方法是将URLSession
包装到协议中,然后在测试中使用符合它的伪造,或使用像OHHTTPStubs这样的库。 This post更详细,再次完全披露,我也写了这个。
希望这会有所帮助。单元测试异步代码并不是一件容易的事情,你应该对它耐心等待并且不断询问是否有些事情似乎不对:)
答案 1 :(得分:2)
与测试GUI类似,我不建议直接在客户端应用程序代码中测试后端。问题主要是来自网络的状态不一致 - 例如,如果您的数据库可以更改,您如何维护稳定的测试?如果你的代码没有改变,但你的测试结果没有改变,那就不好了。
相反,您对测试虚假数据非常正确。我建议将你的网络服务置于某种界面之后,然后根据你想要的测试策略,以某种方式提供测试双打,并实现"整个测试过程中的界面。
不是直接测试应用程序的网络交互,而是使用预定义的域模型对象测试域层(就好像它们已经从网络JSON / XML转换而来),可以让您更精确地控制测试,这很好。
我建议这样做的另一个原因是为了保护您的时间和理智。保持可预测的,精确的测试可能很难,特别是当您重构应用程序代码时。增加潜在的外部性是一种风险。
答案 2 :(得分:0)
要从@drhr添加点,请考虑使用Facade模式抽象出网络细节。在构造函数中传递包含网络功能的特定实现,即在实时中,使用网络的代码,在测试中,代码存根网络。
此类单元测试的目的是测试代码如何响应来自网络代码的不同响应,您不打算在此测试实际的网络元素(如果需要,可以在集成/系统测试中完成) )。
答案 3 :(得分:0)
我会使用模拟来伪造URLSession,真正的单元测试和加速开发http://ocmock.org/是一个很好的工具。如果您仍想测试URLSession,可以使用下面的代码,它使用XCT期望和线程。与其他答案不同,此测试要求测试中的函数使用completition处理程序在其中运行URLSession,该处理程序将设置布尔值(称为myFlag)。函数本身根本不是异步的,只是URLSession调用的一部分。我们将不得不使用一些不同的期望:
func testAFunc(){
let expect = expectation(description: "...")
DispatchQueue.global(qos: .userInitiated).async { // 1
let myObject = MyObject()
myObject.myFuncUnderTestWhichCallURLSession() // the function which calls URLSession
while(true){
if( myObject.myFlag == nil ){
XCTAssertEqual(true, myObject.myFlag)
expect.fulfill()
break;
}
}
}
waitForExpectations(timeout: 5000) { error in
// ...
}
为什么是线程?期望值得称之为满足。在这个例子中,我们没有直接调用异步代码,所以我无法调用其中的实现。在线程内部更改标志(在URLSession响应之后)时使用while循环来调用它,以便它可以到达waitForExpectations。