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

How to Unit Test asynchronous functions that uses Promise Kit

我是否会如此倾向于寻求帮助和/或不同的观点,以了解如何在我的 Viewcontroller 上使用 promise 工具包对后端服务器调用 HTTP 请求的函数进行单元测试 returns JSON 然后解码成需要的数据类型然后映射。

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

func getVantage(stockId: String) {
        firstly {
            self.view.showLoading()
        }.then { _ in
            APIService.Chart.getVantage(stockId: stockId)
        }.compactMap {
            return [=10=].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 kit 函数,如下所示:

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 是 returns 来自请求的值的块,因此我无法在单元测试中形成完成块在传统的 Http 请求中,如:

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

    testExpectation.fulfill()
})

 

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

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

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

阻止您对函数进行正确单元测试的第一件事是 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 [=10=].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 [=13=].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)
}