无法在xcode项目中测试网络呼叫

时间:2019-02-14 11:57:15

标签: ios swift xcode unit-testing xctest

我是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)


    }

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

2 个答案:

答案 0 :(得分:1)

您不应在单元测试中进行网络调用。单元测试旨在测试隔离的单个类。因此,您应该将网络呼叫或inject your dependencies存入要测试的类中(在您的情况下为HomeInteractor)。另外,您的单元测试需要快速运行,在单元测试内进行网络调用会相反。

Here上有一篇文章可以帮助您创建良好的单元测试。

答案 1 :(得分:0)

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

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

Enrique Bermúdez notes一样,您应始终避免在单元测试或集成测试中进行网络调用。击中服务器会使测试运行变慢,并且测试应为fast,以便您可以快速获得反馈。最重要的是,您希望测试是可预测的,但是由于多种原因,进行网络呼叫可能会失败,例如超时,服务器关闭或连接断开。

我知道使自己摆脱对服务器的依赖以及AWS开发工具包的第三方代码的最好方法是在其前面放置一个协议,并用测试倍数替换它。

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

现在,您在第三方和它发出的网络请求之间都有一个抽象层。我们可以构建自己的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
    }
  }
}

根据要测试的行为,您可能会或可能不希望添加探测值来捕获传递给初始化的launchParam。

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

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开发工具包初始化返回false或错误的情况编写测试。如果我们一直按实际的AWS终端节点,就无法做到这一点,因为我们无法控制其响应方式。

附加说明

在协议中包装仅公开我们应用程序感兴趣的API的第三方依赖关系是有价值的,不仅因为它允许我们提供测试双精度并更好地测试代码如何与依赖关系进行交互,而且还因为它使我们不能如果依赖关系发生变化,则必须更改我们的代码,只有包装。另请参见dependency inversion principle

调用包装协议Wrapper是一种气味。理想情况下,您将使用一个名称来捕获所使用的第三方功能的子集。

我们编写的测试检查didSdkInitilized是否设置为true。我想问的问题是“这是HomeInteractor的实际行为,还是只是实现细节?”在不了解HomeIterator应该做什么的情况下很难回答,但是我的猜测是didSdkInitilized只是实现细节,该方法的实际行为是调用onSDKInitlized()或{{1} }在其演示者上。

编写针对行为而不是实现细节的测试总是更好。当您想要重构代码时,即针对实现细节的测试会妨碍您的工作,即在不改变行为方式的情况下更改其实现。从长远来看,针对行为的测试可以帮助您。

在您的情况下,测试第三方SDK成功初始化后onError(errorText:)是否调用演示者的一种可能方法是对演示者使用测试双精度,然后检查其方法是否已被调用。有关此方法here的更多信息。


希望这会有所帮助。如果您想进一步了解Swift中的测试,请通过@mokagio在Twitter上打我。