Swift TDD & async URLSession - 如何进行测试?

4

我想要熟悉TDD。如何测试异步的URLSession调用?在哪个阶段使用哪个XCAssert更好?

我的第一个想法是创建一个函数,该函数内部有URLSession,并在此函数内部将bool标志设置为true,然后在XCAssertTrue中进行测试。或者另一个想法是在调用包含USLSession代码的函数后同步返回虚假数据。

4个回答

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

您可以在此博客文章中了解更多有关这种方法的信息。注意,我是作者。
现在,关于您的建议:
首先我想创建一个函数,该函数内部包含URLSession,并在此函数内部将布尔标志设置为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,我想在单元测试涉及网络代码时留下一个警告。在单元测试中进行实际的网络调用通常不是一个好主意。实际上,它将测试与网络可用并返回预期结果捆绑在一起,并使测试变慢。从单元层面上讲,我们希望我们的测试被隔离、确定性和快速(更多内容参见这里)。命中网络与这些目标不符。避免命中网络的一个好方法是通过将URLSession封装到协议中并在测试中使用符合该协议的伪造对象,或者使用类似OHHTTPStubs的库进行存根。这篇文章更详细地介绍了它,全盘托出,我也写了这篇文章。希望这可以帮助到您。对异步代码进行单元测试并不是微不足道的,您应该有耐心,并在某些情况下不断提问,如果发现有问题 : )

1
对于更复杂的测试(例如,如果您正在解析和验证异步操作的结果),您可以在块的开头使用 defer { expectation.fullfill() },允许在块内使用多个返回语句(guards / etc),同时仅编写一次 fullfill()。 - Abhi Beckert
@AbhiBeckert 很棒!谢谢你的建议。 - mokagio

2

与测试GUI类似,我不建议从客户端应用程序代码直接测试后端。问题主要是来自于网络的不一致状态 - 如果例如您的数据库可以更改,那么如何保持稳定的测试呢?如果您的代码没有变化,但您的测试结果有所变化,那就不好了。

相反,您对测试虚假数据是正确的。我建议将网络服务放在某种接口后面,然后根据您想采用的测试策略,在整个测试过程中提供“实现”该接口的测试替身。

与其直接测试应用程序的网络交互,不如使用预定义的领域模型对象(仿佛它们已经从网络JSON/XML转换)测试您的领域层,这样可以更精确地控制测试,这很好。

我建议这样做的另一个原因是为了保护您的时间和理智。维护可预测、精确的测试已经很难了,尤其是当您重构应用程序代码时。在此基础上添加一个可能会发生变化的外部因素是一种风险。


0

除了@drhr提到的要点,考虑使用门面模式来抽象化网络细节。在构造函数中传入一个特定的实现,包装网络功能,即在实际操作中使用Networking,在测试中使用存根网络代码。

这种类型的单元测试的目的是测试您的代码如何响应来自网络代码的不同响应,您的目标不是在这里测试实际的网络元素(如果需要可以在集成/系统测试中完成)。


0

我会使用模拟对象来伪造URLSession,进行真正的单元测试并加速开发。http://ocmock.org/是一个很好的工具来实现这一点。如果你仍然想测试URLSession,你可以使用下面的代码,它使用XCT expectations和线程。与其他答案不同,这个测试期望被测试的函数在其中运行URLSession,并设置布尔值(称为myFlag)的完成处理程序。函数本身根本不是异步的,只是由URLSession调用的部分。我们将不得不使用稍微不同的expectations:

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
        // ...
    }

为什么要使用线程?expectations期望被调用并实现。在这个例子中,我们没有直接调用异步代码,因此我无法在其中调用fulfill。使用while循环,在线程内当标志改变(在URLSession响应后)时调用它,以便它可以达到waitForExpectations。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接