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

Posted

技术标签:

【中文标题】无法在 xcode 项目中测试网络调用【英文标题】:cannot test network call inside xcode project 【发布时间】:2019-02-14 11:57:14 【问题描述】:

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


    

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

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

嘿,谢谢您的回答,您能否给出一些代码示例,说明您对网络调用的存根意味着什么? (我的大部分应用程序只是从框架调用这个调用,所以如果我不测试这个,我就没有其他东西要测试了)【参考方案2】:

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

它使用 对第三方的隐式依赖,即 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 ?? "")
      
    
  

现在您在第 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
    
  

根据您要测试的行为,您可能希望也可能不希望添加探测值来捕获传递给初始化的启动参数。

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

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 是一种异味。理想情况下,您应该使用一个名称来捕捉您正在使用的第 3 方功能的子集。

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

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

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


希望这会有所帮助。如果您想了解更多有关 Swift 测试的信息,请在 Twitter 上联系我@mokagio。

【讨论】:

以上是关于无法在 xcode 项目中测试网络调用的主要内容,如果未能解决你的问题,请参考以下文章

在 Xcode 8.3.2 的 UI 测试用例中转换为当前的快速语法“无法调用非函数类型‘XCUIElement’的值”时出错

ios真机弱网测试

Mac 模拟弱网测试

XCode 7 beta 6 UI 测试 - 无法选择列表中的元素

为啥编译我的 XCode 项目可以使用 Apple-(B)uild 并且无法使用 Apple-(U)nit 测试编译?

如何在 Xcode 4.5“命令行工具”项目中设置工作逻辑单元测试目标?