问题描述
我是否可能会如此倾向于就如何对我的 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)
}