Testing MVVM with RxSwift code
I don’t wanna waste your time explaining why writing tests is important since I suppose you already know enough about this if you came across this article.
I recommend reading the chapter about testing from Uncle Bobs Clean Code where he explains why is testing important in his own way, and why is it important to keep tests clean. He also explains five rules called F.I.R.S.T. in a bit more details. Let’s just keep in mind that test should be Fast, Independent, Repeatable, Self Validating, Timely.
We are using MVVM architecture, and you can read more about it in my blog post.
There are many different types of testing but this article is going to focus on the unit and integration tests.
My experience: How writing tests made me a better developer
You can absolutely skip this chapter if you are here just for the code or you want to see the way we implemented tests with RxSwift. I won’t even be mad, I just want to share my story about getting into writing tests and why it was useful to me.
I’m not ashamed to admit that this was my first big project where we decided that we are going to write tests. In the college, there were at least five courses where I was learning about the importance of testing, rules, benefits, etc., but I never fully understood how powerful testing can be. I even wrote tests for the several college projects but I was always writing them with just making them pass in mind, never have I thought about benefiting from them. They were there just so I can say “I’m writing test“ to make me look like a better developer or to meet the project requirements. How young and naive was I? And that was not that long ago.
With getting the new project we decided that this will be the project where we as a team want to take our skills to the new level, with great code coverage, unit, integration, and UI test, and same dedication for tests as for code, architecture, and design.
Writing first tests
So a decision was made and the time has come to start writing tests. We are not doing Test Driven Development so what I tried to do is write tests for the Login feature that I was working on already. The feature was almost done and I wanted to write tests for it to make sure if anything changes in the future, the changes don’t break anything, and if they do, to pinpoint to what is broken. Figuring out why would be much easier then.
- First time I created a new test class in the project using RxSwift I was pretty confused. I wanted to test observables in the view model, but I had no idea how to do it, how to make it a unit and not an integration test, how to simulate events from view controller etc.
I tried to write unit tests for LoginViewModel. Let’s see what this means.
- Unit tests — the lowest level of software testing where individual components — units are being tested. In this case, our unit is LoginViewModel, and writing tests for this unit mean that all the dependencies must be mocked and all the tests must depend only on LoginViewModel code.
- What that means in our MVVM architecture is that we want to tests each layer and every component for itself. So each ViewController, ViewModel , and Model has to have its own tests. For each component in our architecture, we need to have an associated test suite. Basically, you want to isolate and test each of your classes separately. All the services, managers etc. should be tested on its own, and have mocks that can be injected into the class that is using them.
- My LoginViewModel was depending on the few classes, like NetworkingService and ValidationService. Something like this.
class LoginViewModel {
private let networkingService = NetworkingService()
private let validationService = ValidationService()
...}
- Looking at the code above makes me sick in the stomach now, because this code is hardly reusable or testable. How do you write mocks for these dependencies? The answer is protocols and dependency injection!
class LoginViewModel {
private let networkingService: NetworkingServiceProtocol
private let validationService: ValidationServiceProtocol
init(networkingService: NetworkingServiceProtocol,
validationService: ValidationServiceProtocol) { self.networkingService = networkingService
self.validationService = validationService
}
...}
Swapping concrete implementations with interfaces allows us to write mocks much easier. Our mocks now just have to implement these protocols and we can do that with ease. And we no longer depend on the implementation of these services in our tests and now these tests can be called unit tests.
Once I wrote all the unit tests I start writing integration tests to see if different parts of the system work as they should when they interact with each other.
Integration tests — the next level of software testing after unit testing, This is the place where you want to test the interaction between integrated components in your system. Take ViewController and ViewModel for example. After we wrote isolated tests for SomeViewController, and for SomeViewModel, we need to test do these components work as they should once they interact with each other. This interaction in our MVVM architecture is implemented using RxSwift.
Writing integration tests now should be easy, like playing with legos, since you just connect components that you want to connect and use mocks for the others.
We have testers dedicated to writing UI tests using Appium so I’m not going to write about that since I have very basic knowledge of this.
Testing Rx code
When we started writing tests for our architecture we got stuck immediately, and the reason for this were bindings of View Controllers and View Models. The way we used to implement these bindings is that we would initialize View Model with Observables we needed to handle, or we would just assign them along the way, something like this:
class SomeViewModel {
var counter: Driver<String>! init(increaseCounterTaps: Observable<Void>) {
counter = increaseCounterTaps
.flatMap
...
}}
or like this:
class SomeViewModel {
var counter: Driver<String>! func setupIncreaseTaps(increaseCounterTaps: Observable<Void>) {
counter = increaseCounterTaps
.flatMap
...
}}
Soon we figured out that it was a nightmare to write tests for something like this. A solution we came up with was to switch from Observables to PublishSubject since it is much easier to simulate event on PublishSubject as all you have to do is:
let somePublishSubject = PublishSubject<Bool>()
somePublishSubject.onNext(true)
Also, this allows us to just bind events from the View Controller to View Model, and we are still using Observables in View Model for events that View Controller can subscribe on.
Example
Let’s pretend we have a View Controller that shows one label with counter value and one button to increase that value. View Model is responsible for handling each tap, increasing the counter and letting View Controller know that label should be updated and what it should be updated with. Also, we are going to write unit tests for both, and integration test to make sure these two together are working as they should.
Download example repo and check it out so you can follow along easier.
testIncreaseButtonTaps_WithOneTapEvent_LabelTextShouldBeOne method in CounterViewModelTests is the best example of simulating events from View Controller.
func testIncreaseButtonTaps_WithOneTapEvent_LabelTextShouldBeOne() { let observer: TestableObserver<String> = scheduler.createObserver(String.self) let correctResult: [Recorded<Event<String>>] = [next(0, "0"), next(1, "1")] sut.counterValue
.subscribe(observer)
.disposed(by: disposeBag) let allEvents = scheduler.createHotObservable([next(1, ())]) allEvents
.bind(to: sut.increaseButtonTaps)
.disposed(by: disposeBag) scheduler.start()
XCTAssertEqual(correctResult, observer.events)}
Here we are creating TestableObserver that we are going to use to compare with some result of type [Recorded<Event<String>>]. Next thing we have to do is simulate some events, and we have to do it at exact time.
As you can see, we are simulating a new tap event at timestamp 1. The expected result is the label to have text “0” at the start and text “1” after the simulated tap. Run tests and check if everything is passing.
Check out CounterViewControllerTests to see how easy it is to inject Mock of a View Model.
Also look at integration tests for this screen to see the way I implemented it.
Thank you for your attention and please let me know if you have a better solution for anything above.