如何对使用 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)
}
我是否会如此倾向于寻求帮助和/或不同的观点,以了解如何在我的 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)
}