如何对使用 Promise Kit 的异步函数进行单元测试

问题描述

我是否可能会如此倾向于就如何对我的 Viewcontroller 上的函数进行单元测试,该函数使用 promise 工具包向后端服务器调用 HTTP 请求,该工具包返回 JSON,然后解码为数据需要的类型,然后映射。

这是用于获取股票价值等的承诺工具包函数之一(在 viewWillAppear 中调用)...

func getVantage(stockId: String) {
        firstly {
            self.view.showLoading()
        }.then { _ in
            APIService.Chart.getVantage(stockId: stockId)
        }.compactMap {
            return $0.dataModel()
        }.done { [weak self] data in
            guard let self = self else { return }
            self.stockValue = Float(data.price ?? "") ?? 0.00
            self.valueIncrease = Float(data.delta ?? "") ?? 0.00
            self.percentageIncrease = Float(data.deltaPercentage ?? "") ?? 0.00
            let roundedPercentageIncrease = String(format: "%.2f",self.percentageIncrease)
            self.stockValueLabel.text = "\(self.stockValue)"
            self.stockValueIncreaseLabel.text = "+\(self.valueIncrease)"
            self.valueIncreasePercentLabel.text = "(+\(roundedPercentageIncrease)%)"
        }.ensure {
            self.view.hideLoading()
        }.catch { [weak self] error in
            guard let self = self else { return }
            self.handleError(error: error)
        }
    }

我想过使用期望来等待,直到在单元测试中调用 promise 工具包函数,如下所示:

func testChartsMain_When_ShouldReturnTrue() {
        
        //arange
        let sut = ChartsMainViewController()
        let exp = expectation(description: "")
        let testValue = sut.stockValue
        
        //Act
        
 -> Note : this code down here doesn't work
 -> normally a completion block then kicks in and asserts a value then checks if it fulfills the expectation,i'm not mistaken xD
-> But this doesn't work using promisekit

        //Assert
        sut.getVantage(stockId: "kj3i19") {
        XCTAssert((testValue as Any) is Float && !(testValue == 0.0))
        exp.fulfill()
        }
        self.wait(for: [exp],timeout: 5)
    }

但问题是 promisekit 是在它自己的自定义链块中完成的,其中 .done 是从请求中返回值的块,因此我无法像在传统的 Http 请求中那样在单元测试中形成完成块,例如:

sut.executeAsynchronousOperation(completion: { (error,data) in
    XCTAssertTrue(error == nil)
    XCTAssertTrue(data != nil)

    testExpectation.fulfill()
})

 

解决方法

您的视图控制器中似乎有大量的业务逻辑,这使得正确测试您的代码变得更加困难(并非不可能,而是更难)。

建议将所有网络和数据处理代码提取到该控制器的(视图)模型中,并通过一个简单的接口公开它。这样,您的控制器就会尽可能地虚拟化,并且不需要太多的单元测试,而且您将把单元测试集中在(视图)模型上。

但那是另一个很长的故事,我偏离了这个问题的主题。

首先阻止您对函数进行正确单元测试的是 APIService.Chart.getVantage(stockId: stockId),因为您无法控制该调用的行为。因此,您需要做的第一件事就是以协议或闭包的形式注入该 api 服务。

以下是闭包方法的示例:

class MyController {
    let getVantageService: (String) -> Promise<MyData>

    func getVantage(stockId: String) {
        firstly {
            self.view.showLoading()
        }.then { _ in
            getVantageService(stockId)
        }.compactMap {
            return $0.dataModel()
        }.done { [weak self] data in
            // same processing code,removed here for clarity 
        }.ensure {
            self.view.hideLoading()
        }.catch { [weak self] error in
            guard let self = self else { return }
            self.handleError(error: error)
        }
    }
}

其次,由于异步调用没有暴露在函数之外,所以很难设置测试期望,以便单元测试一旦知道就可以断言数据。此函数的异步调用仍在运行的唯一指标是视图显示加载状态,因此您可以利用它:

let loadingPredicate = NSPredicate(block: { _,_ controller.view.isLoading })
let vantageExpectation = XCTNSPredicateExpectation(predicate: loadingPredicate,object: nil)

通过上述设置,您可以使用期望来断言您对 getVantage 的期望:

func test_getVantage() {
    let controller = MyController(getVantageService: { _ in .value(mockedValue) })
    let loadingPredicate = NSPredicate(block: { _,_ !controller.view.isLoading })
    let loadingExpectation = XCTNSPredicateExpectation(predicate: loadingPredicate,object: nil)

    controller.getVantage(stockId: "abc")
    wait(for: [loadingExpectation],timeout: 1.0)

    // assert the data you want to check
}

它很混乱,也很脆弱,将其与将数据和网络代码提取到(视图)模型进行比较:

struct VantageDetails {
    let stockValue: Float
    let valueIncrease: Float
    let percentageIncrease: Float
    let roundedPercentageIncrease: String
}

class MyModel {
  let getVantageService: (String) -> Promise<VantageDetails>

  func getVantage(stockId: String) {
        firstly {
            getVantageService(stockId)
        }.compactMap {
            return $0.dataModel()
        }.map { [weak self] data in
            guard let self = self else { return }
            return VantageDetails(
                stockValue: Float(data.price ?? "") ?? 0.00,valueIncrease: Float(data.delta ?? "") ?? 0.00,percentageIncrease: Float(data.deltaPercentage ?? "") ?? 0.00,roundedPercentageIncrease: String(format: "%.2f",self.percentageIncrease))
        }
    }
}

func test_getVantage() {
    let model = MyModel(getVantageService: { _ in .value(mockedValue) })
    let vantageExpectation = expectation(name: "getVantage")

    model.getVantage(stockId: "abc").done { vantageData in
        // assert on the data

        // fulfill the expectation
        vantageExpectation.fulfill() 
    }
   
    wait(for: [loadingExpectation],timeout: 1.0)
}