在Swift中对MVVM架构进行单元测试
为确保新代码不会破坏已经实施的旧代码,最佳实践是编写单元测试。 对于应用程序体系结构,编写这些测试可能是一个挑战。 遵循MVVM模式, 如何对视图及其viewModel进行单元测试? 这就是我想在这里使用依赖注入的内容。
MVVM体系结构的主要优点是将逻辑解耦并保持关注点分离。 每个类和文件都有特定的目标。 该代码保持模块化,可重用且易于测试。 测试用例应采用相同的逻辑:编写的每个测试仅覆盖一个用例,而仅覆盖一个用例,请隔离逻辑并确保其正常工作。
在此示例中,我还将保持关注点分离,一次测试一个元素。 我的意思是,对于MVVM模式,我至少要有3个测试文件:一个用于我的模型,一个用于我的视图,一个用于我的viewModel。
但是在深入研究代码之前,如果您不熟悉MVVM,我建议您先看一下我最近编写的实现MVVM模式的介绍。
准备您的viewModel
为了能够针对我的viewModel运行测试,我需要能够使用其他服务,并能够通过依赖注入来模拟我的服务。 第一步是为服务创建一个协议,然后将其实现到当前服务,最后更新视图模型。
我这就是我的协议和服务的样子
protocol CurrencyServiceProtocol : class {
func fetchConverter(_ completion: @escaping ((Result) -> Void))
}
final class CurrencyService : RequestHandler, CurrencyServiceProtocol { ... }
然后我可以在viewModel中使用依赖项注入和默认参数
weak var service: CurrencyServiceProtocol?
init(service: CurrencyServiceProtocol = CurrencyService.shared, dataSource : GenericDataSource?) {
self.dataSource = dataSource self.service = service
}
func fetchCurrencies(_ completion: ((Result) -> Void)? = nil) {
guard let service = service else {
completion?(Result.failure(ErrorResult.custom(string: "Missing service")))
return
}
...
现在我们准备编写测试了。
ViewModel测试用例
在我的测试方面,第一件事是为每个特定的测试准备课程。 由于我的viewModel可以使用服务和数据源,因此我将在此处进行模拟。
class CurrencyViewModelTests: XCTestCase {
var viewModel : CurrencyViewModel!
var dataSource : GenericDataSource!
fileprivate var service : MockCurrencyService!
override func setUp() {
super.setUp()
self.service = MockCurrencyService()
self.dataSource = GenericDataSource()
self.viewModel = CurrencyViewModel(service: service, dataSource: dataSource)
}
override func tearDown() {
self.viewModel = nil
self.dataSource = nil
self.service = nil
super.tearDown()
}
}
为了模拟服务,我实现了相同的先前协议,使用局部变量伪造数据。
fileprivate class MockCurrencyService : CurrencyServiceProtocol {
var converter : Converter?
func fetchConverter(_ completion: @escaping ((Result) -> Void)) {
if let converter = converter {
completion(Result.success(converter))
} else {
completion(Result.failure(ErrorResult.custom(string: "No converter")))
}
}
}
现在我们准备运行我们的第一个测试
func testFetchWithNoService() {
// giving no service to a view model
viewModel.service = nil
// expected to not be able to fetch currencies
viewModel.fetchCurrencies { result in
switch result {
case .success(_) :
XCTAssert(false, "ViewModel should not be able to fetch without service")
default:
break
}
}
}
查看测试案例
在我的示例应用程序中,视图由UIViewController表示,但是除了实际的UITableView及其数据源外,没有太多要测试的内容。
因此,我以同样的方式将精力集中在这两个元素之间的关系上。 这是它的味道。
class CurrencyDataSourceTests: XCTestCase {
var dataSource : CurrencyDataSource!
override func setUp() {
super.setUp()
dataSource = CurrencyDataSource()
}
override func tearDown() {
dataSource = nil
super.tearDown()
}
func testValueInDataSource() {
// giving data value
let euroRate = CurrencyRate(currencyIso: "EUR", rate: 1.14)
let dollarRate = CurrencyRate(currencyIso: "EUR", rate: 1.40)
dataSource.data.value = [euroRate, dollarRate]
let tableView = UITableView()
tableView.dataSource = dataSource
// expected one section
XCTAssertEqual(dataSource.numberOfSections(in: tableView), 1, "Expected one section in table view")
// expected two cells
XCTAssertEqual(dataSource.tableView(tableView, numberOfRowsInSection: 0), 2, "Expected no cell in table view")
}
模型测试用例
这里的最佳实践是仅测试模型的解析方面。 但是,我当前的iOS应用程序不允许直接将JSON数据注入到我的模型中。 因此,我在这里做了一些尝试,并测试了我的主要Parser服务和模型。
在您的生产应用程序上,最好在这里进行两个测试,一个用于解析器,一个用于模型。 原因很简单:如果明天更改解析器,则也不必重写模型测试用例。
一个小提示:为避免调用任何网络,我只是向应用程序中添加了一个JSON文件来模拟来自服务器的数据。 如果您想与api的旧版本复古兼容,这是一件好事。 但是,新的api版本也需要一个新的JSON文件和测试用例。
class CurrencyTests: XCTestCase {
func testParseCurrency() {
// giving a sample json file
guard let data = FileManager.readJson(forResource: "sample") else {
XCTAssert(false, "Can't get data from sample.json")
return
}
// giving completion after parsing
// expected valid converter with valid data
let completion : ((Result) -> Void) = { result in
switch result {
case .failure(_):
XCTAssert(false, "Expected valid converter")
case .success(let converter):
XCTAssertEqual(converter.base, "GBP", "Expected GBP base")
XCTAssertEqual(converter.date, "2018-02-01", "Expected 2018-02-01 date")
XCTAssertEqual(converter.rates.count, 32, "Expected 32 rates")
}
}
ParserHelper.parse(data: data, completion: completion) }
总之 ,通过依赖项注入方法和一些小的更新,我们设法测试了MVVM体系结构的每个部分,还显示了保持关注点分离的重要性。
但是,我的示例应用程序的内容非常小而简单。 添加更多的屏幕和导航可能使跟踪测试方法变得更加困难。 当我尝试对单元进行单元测试并在其UI生命周期(1)之外调用UI方法时,也遇到了相同的问题。
我认为这就是警告: 如果要测试的功能变得复杂,最好将其重构为易于维护的小片段 。
这个单元测试示例都可以在Github上获得。
(1)NSInternalInconsistencyException滚动UITableViews·问题#1007·kif-framework / KIF·GitHub