Unit Testing your MVVM architecture in Swift
To be sure new code won’t break old one already implemented, it’s best practice to write unit tests. When it comes to app architectures, it can be a challenge to write those tests. Following an MVVM pattern, how to unit test a view and its viewModel? That’s what I would like to cover here using dependency injection.
The main advantage of MVVM architecture is decouple logic and keep a separation of concerns. Each class and files has a specific goal. The code stays modular, reusable and easy to test. The same logic should apply test cases: each test is written to cover one use case and one only, isolate the logic and be sure it’s working properly.
In this example, I’ll keep the separation of concerns too, testing one element at a time. I mean here that for a MVVM pattern, I’ll have at least 3 test files: one for my model, one for my view and one for my viewModel.
But before diving into the code, if you’re not familiar with MVVM, I would encourage you to have a look into an introduction to implement MVVM pattern I recently wrote.
Prepare your viewModel
To be able to run test against my viewModel, I needed to be able to use another service, to be able to mock up my service by dependency injection. First step is to create a protocol for the service, then implementing it to the current service and finally update the view model.
I Here is what it looks like my protocol and service
protocol CurrencyServiceProtocol : class {
func fetchConverter(_ completion: @escaping ((Result<Converter, ErrorResult>) -> Void))
}
final class CurrencyService : RequestHandler, CurrencyServiceProtocol {
...
}
Then I can use dependency injection and default parameter in my viewModel
weak var service: CurrencyServiceProtocol?
init(service: CurrencyServiceProtocol = CurrencyService.shared, dataSource : GenericDataSource<CurrencyRate>?) {
self.dataSource = dataSource
self.service = service
}
func fetchCurrencies(_ completion: ((Result<Bool, ErrorResult>) -> Void)? = nil) {
guard let service = service else {
completion?(Result.failure(ErrorResult.custom(string: "Missing service")))
return
}
...
Now we are ready to write our test.
ViewModel test cases
On my test side, first thing is to prepare the class for each specific test. As my viewModel can use a service and dataSource, I’ll mock up both here.
class CurrencyViewModelTests: XCTestCase {
var viewModel : CurrencyViewModel!
var dataSource : GenericDataSource<CurrencyRate>!
fileprivate var service : MockCurrencyService!
override func setUp() {
super.setUp()
self.service = MockCurrencyService()
self.dataSource = GenericDataSource<CurrencyRate>()
self.viewModel = CurrencyViewModel(service: service, dataSource: dataSource)
}
override func tearDown() {
self.viewModel = nil
self.dataSource = nil
self.service = nil
super.tearDown()
}
}
To mockup my service, I implemented the same previous protocol, faking data with a local variable.
fileprivate class MockCurrencyService : CurrencyServiceProtocol {
var converter : Converter?
func fetchConverter(_ completion: @escaping ((Result<Converter, ErrorResult>) -> Void)) {
if let converter = converter {
completion(Result.success(converter))
} else {
completion(Result.failure(ErrorResult.custom(string: "No converter")))
}
}
}
Now we’re ready to run our first test
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
}
}
}
View test cases
In my sample app, the view is represented by a UIViewController, however, there is not much to test in it except the actual UITableView and its datasource.
So in the same way I focus my effort on the relationship between those two elements. Here is a taste of it.
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")
}
Model test cases
Here best practice would be to test only parsing side of my model. However, my current iOS application doesn’t allow to inject a JSON data straight away to my model. So I cut a bit the corner there and test my main Parser service and model.
On your production app, it’s best to have two tests here, one for the parser, one for the model. The reason is quite simple: if you change your parser tomorrow, you don’t have to rewrite your model test cases too.
A small tip: to avoid calling any network, I just added a JSON file to my app to mockup the data coming from the server. It’s a good thing if you want to be retro compatible with older version of your api. However a new api version would need a new JSON file and test case for it too.
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<Converter, ErrorResult>) -> 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)
}
In conclusion, with dependency injection approach and some small updates, we manage to test each part of our MVVM architecture, showing also how important it can be to keep a separation of concerns.
However, the content of my sample app is quite small and simple. Adding more screens and a navigation can make it harder to keep track of how to test. I had this same issues when I tried to unit test cells and calling UI methods out of its UI lifecycle(1).
I believe that’s where is the warning: if a function becomes to complex to test, it’s probably better to refactor it in small pieces easily maintainable.
This unit test examples are all available on Github.
(1) NSInternalInconsistencyException scrolling UITableViews · Issue #1007 · kif-framework/KIF · GitHub