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

问题描述 投票:2回答:2

我是ios Unit Testing的新手,这是我第一次真正做到这一点。

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

我试图通过调用其中一个函数来测试框架的业务逻辑,但函数只是通过实际调用,并且永远不会进入回调。

这是我试图检查的框架内的实际功能>

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)


    }

但它始终失败,因为它永远不会成功或失败的回调。我在这做错了什么?

ios swift xcode unit-testing xctest
2个回答
1
投票

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

Here有一篇帖子可以帮助你创建好的单元测试。


0
投票

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

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

作为Enrique Bermúdez notes,您应该始终避免在单元或集成测试中进行网络调用。命中服务器会使测试运行速度变慢,测试应该是fast,这样您就可以快速获得反馈。最重要的是,您希望测试是可预测的,但进行网络呼叫可能会因任何原因而失败,例如超时,服务器关闭或连接中断。

我知道将自己与服务器的依赖关系和AWS SDK的第三方代码隔离开的最好方法是在其前面放置协议,并将其替换为test double。

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

根据您要测试的行为,您可能想要也可能不想添加探测值来捕获传递给初始化的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初始化返回false或错误的情况编写测试。如果我们不断触及实际的AWS端点,我们就无法做到这一点,因为我们无法控制它的响应方式。

Additional Notes

在仅公开我们的应用程序感兴趣的API的协议中包装第三方依赖项是有价值的,不仅因为它允许我们提供测试双精度并更好地测试代码如何与依赖项交互,还因为它允许我们不必更改我们的代码如果依赖项改变,只有包装器。另见dependency inversion principle

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

我们写的测试检查didSdkInitilized是否设置为true。我想问你的问题是“这是HomeInteractor的实际行为,还是只是一个实现细节?”如果不了解更多关于HomeIterator应该做什么的话,很难回答,但我的猜测是didSdkInitilized只是一个实现细节,该方法的真实行为是在其演示者上调用onSDKInitlized()onError(errorText:)

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

在您的情况下,测试HomeInteractor在第三方SDK成功初始化时是否调用演示者的可能方法是使用测试双重作为演示者,然后检查其方法是否已被调用。更多关于这种方法here


希望这可以帮助。如果你想在Swift中更多地谈论测试,请在@mokagio的Twitter上发帖。

© www.soinside.com 2019 - 2024. All rights reserved.