无法在 xcode 项目中测试网络调用

cannot test network call inside xcode project

我是 ios Unitesting 的新手,这是我第一次真正这样做。

我的应用程序正在使用一个正在执行一些网络调用的框架,

我正在尝试通过调用其中一个函数来测试框架的业务逻辑 但该函数只是通过了实际调用,并没有进入回调。

这是我正在尝试检查的框架内的实际功能 >

func initSDK () {
        let launchParams: [String:String] = [
            kAWSDKUrl:  WhiteLabeler.localizedStringForKey(key: "baseSDKUrl", comment: "-", defaultValue: "https://isr-lap-tst2.americanwell.com:8443/"),
            kAWSDKKey:  WhiteLabeler.localizedStringForKey(key: "SDKserviceKeyForIos", comment: "-", defaultValue: "TriageApp"),
            kAWSDKBundleID: Bundle.main.bundleIdentifier!
        ]

        AWSDKService.initialize(withLaunchParams: launchParams) {[weak self] (success, error) in
            if success {
                self?.myPresenter?.onSDKInitlized()
                self?.didSdkInitilized = true
            } else {
                self?.myPresenter?.onError(errorText: error?.localizedDescription ?? "")
            }
        }
    }

这是我的测试用例:

import XCTest
@testable import TriageFramework

class Virtual_First_Tests: XCTestCase {

    override func setUp() {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample1() {
        let homeInteractor: HomeInteractor = HomeInteractor();
        var didInitlized = homeInteractor.didSdkInitilized
        homeInteractor.initSDK()

        sleep(2)

        didInitlized = homeInteractor.didSdkInitilized
        XCTAssertTrue(didInitlized)


    }

但它总是失败,因为它永远不会进入成功或失败的回调。 我在这里做错了什么?

您不应在单元测试中进行网络调用。单元测试旨在测试孤立的单个 classes。因此,您应该将网络调用或 inject your dependencies 存根到您正在测试的 class 中(在您的情况下为 HomeInteractor)。此外,您的单元测试需要 运行 快速,在单元测试中进行网络调用会适得其反。

Here 有一个 post 可以帮助您创建良好的单元测试。

关于您要测试的代码,有几点需要指出:

  • 它使用隐式依赖第三方,aws sdk 服务。具有隐式依赖项的组件的行为更难测试,因为您无法直接控制它们
  • 它进行异步调用。也就是说,部分行为发生在 initialize 方法的闭包回调中。这意味着测试需要考虑到他们必须等待闭包被调用的事实。

As , you should always avoid to make network calls in unit or integration tests. Hitting a server makes the tests run slower, and tests should be fast 这样您就可以得到快速的反馈。最重要的是,您希望您的测试是可预测的,但是进行网络调用可能会由于多种原因而失败,例如超时、服务器停机或连接中断。

据我所知,使自己免受对服务器和 AWS SDK 的第三方代码的依赖的最好方法是在其前面放置一个协议,并将其替换为测试替身。

protocol AWSSDKWrapper {

  static func initialize() // this should actually match the signature of the
                           // initialize method on AWSSDKService, but I don't
                           // know how it looks like
}

extension AWSSDKService: AWSSDKWrapper { }

然后您可以在您的类型的 init 中注入对 AWS 的依赖。

class HomeInteractor {

  private let awsSDKWrapper: AWSSDKWrapper

  init(awsSDKWrapper: AWSSDKWrapper = AWSSDKService.self) {
    self.awsSDKWrapper = awsSDKWrapper
  }

  func initSDK () {
    let launchParams: [String:String] = // ...

    awsSDKWrapper.initialize(withLaunchParams: launchParams) {[weak self] (success, error) in
      if success {
        self?.myPresenter?.onSDKInitlized()
        self?.didSdkInitilized = true
      } else {
        self?.myPresenter?.onError(errorText: error?.localizedDescription ?? "")
      }
    }
  }
}

现在你在第 3 方和它发出的网络请求之间有了一个抽象层。我们可以构建自己的 test double 来控制测试中的行为。

struct AWSSDKStub: AWSSDKWrapper {

  let result: Result<Bool, NSError>  // I'm assuming the type of error the AWSSDK
                                     // callback returns is NSError.
                                     // I'm also assuming you have Swift 5 Result
                                     // available, if you don't check this Gist for
                                     // a lightweight drop-in replacement

  init(succeeding: Bool) {
    self.result = .success(succeeding)
  }

  init(error: NSError) {
    self.result = .failure(error)
  }

  func initialize(/* again not sure how the arguments look like */) {
    switch result {
      case .success(let succeeded): callback(succeeded)
      case .failure(let error): error
    }
  }
}

根据您想要测试的行为,您可能想要也可能不想添加探测值来捕获传递给初始化的 launchParams。

现在让我们使用这个对象来控制测试中的行为。

func testAWSSDKInitializeSuccess() {
  let homeInteractor = HomeInteractor(awsSDWWrapper: AWSSDKStub(succeeding: true))

  // Because the test is asynchronous we need to setup the expectation _before_
  // calling its method.
  // 
  // More here: https://www.mokacoding.com/blog/xctest-closure-based-expectation/
  let predicate = NSPredicate(block: { any, _ in
    return (any as? HomeIterator)?.didSdkInitilized == true
  })
  _ = self.expectation(for: predicate, evaluatedWith: homeIterator, handler: .none)

  homeIntera.initSDK()

  waitForExpectations(timeout: 1, handler: .none)
}

这种方法的美妙之处在于,我们还可以为 AWS SDK 初始化 returns 错误或错误的情况编写测试。如果我们一直访问实际的 AWS 端点,我们就无法做到这一点,因为我们无法控制它的响应方式。

补充说明

将第三方依赖项包装在仅公开 API 我们的应用程序感兴趣的协议中很有价值,不仅因为它允许我们提供测试替身并更好地测试代码如何与依赖项交互,而且因为它允许我们在依赖项发生变化时不必更改代码,只需更改包装器即可。另见 dependency inversion principle.

调用包装器协议 Wrapper 是一种味道。理想情况下,您会使用一个名称来捕获您正在使用的第 3 方功能的子集。

我们编写的测试检查 didSdkInitilized 是否设置为 true。我想问你的问题是 "is this the actual behaviour of HomeInteractor, or is it just an implementation detail?" 在不了解 HomeIterator 应该做什么的情况下很难回答,但我的猜测是 didSdkInitilized 只是一个实现细节,真正的行为该方法是在其演示者上调用 onSDKInitlized()onError(errorText:)

最好编写专注于行为而不是实现细节的测试。当您想要重构代码时,专注于实现细节的测试会妨碍您,即在不改变其行为方式的情况下更改其实现。专注于行为的测试可以帮助您长期 运行.

在您的情况下,当第三方 SDK 成功初始化时,测试 HomeInteractor 是否调用演示者的一种可能方法是对演示者使用测试替身,然后检查其方法是否已被调用.有关此方法的更多信息 here


希望这对您有所帮助。如果您想更多地讨论 Swift 中的测试,请在 Twitter 上联系我 @mokagio