Unit testing RxSwift apps is the topic I want to talk about today. This is the last part of my series ‘How to use RxSwift with MVVM’, where we have learned to use RxSwift by implementing the Friends application. The only remaining thing is to unit test the application. Unit testing of RxSwift applications is pretty similar to unit testing a normal swift application. Once again, the biggest change is that we handle all the callbacks and data updates with observers. In this post, we’ll see how to:
- Handle Observables and subscribe to events.
- Mock network layers for unit testing.
- Handle data validation in unit tests.
All the code that we’ll test is inside view models, so you’ll also learn how to unit test the view model. In case you are not that familiar with the concept of unit testing, I suggest that you read my previous post about unit testing view models. You’ll get all the basic information of unit testing and also a little friendly reminder of why you should always unit test your applications! 😄
We will learn these things by unit testing an application called Friends. The Friends application is an application that I implemented using MVVM pattern. With the Friends app, you can download a list of friends and display them to the user using
UITableView. You can also create, update, and delete friends using the app. It’s a simple app with just enough features to go through many of the basic things that you come across when developing an iPhone app. I first wrote the Friends app without RxSwift, and then wanted to see how much the code changes if I use RxSwift. In case you want to know more about the implementation of a pure MVVM app, check my posts about MVVM pattern with a Swift app. And if you want to see how the app we are testing today is implemented, check out this post: How to use RxSwift with MVVM.
All the codes can be downloaded from GitHub. Just remember to check out the RxSwift branch. But now, let’s get down to business!
Unit Testing RxSwift App
We’ll start by checking how to unit test
FriendsTableViewViewModel. It’s a class that handles displaying a list of friends to the user.
FriendsTableViewViewModel makes network requests, the first thing we need to do is to mock the network layer for testing. Generally, we don't want unit tests to make network requests because:
- It might take some time to get an answer from the server, which makes running the tests slow.
- The test might fail because of the network or the server, which makes it impossible to verify the result reliably.
By mocking the network layer, we can return a suitable answer for the current test case.
Mocking Network Layers
AppServerClient is the class that handles all the networking in the app. If we open the class, we’ll see that it has a function called
getFriends. This is the function that we want to override for the first tests. It downloads a list of friends and then we display the list to the user. The function definition looks like this:
We could define a protocol that has all the same function definitions that
AppServerClient has. Then we could make both the
AppServerClient and our mock implementation to conform to that protocol. But since we don’t have the protocol already available, we'll use the good old inheritance instead.
MockAppServerClient inherits from
AppServerClient and we have overridden the
getFriendsFunction. The first thing we do inside the function, is that we create an
Observable that we return from the function. We pass a block for the create function and use Switch for a variable named
getFriendsResult is the variable that we use to define the different results for our network requests. In success case, it contains a list of friends and in failure case, it contains an error. Later, we’ll check how to define the value for our tests. Inside the switch statement, we have defined
.failure cases and emit
.onNext with a list of friends or a
.onError with an error value to the subscriber. We have defined the
getFriendsResult as an
Optional since we don’t want to define an initializer for the mock class. That is why we also need to define the
.none case in the function.
And finally, we’ll return a dummy disposable that the
Observable.create needs as a return value. The first step in unit testing an RxSwift app is done! Next, let’s write our first tests. We'll use dependency injection to pass the mock network layer we just created and also set the
getFriendsResult to match our test case assertions.
Unit Testing RxSwift — FriendsTableViewModel
When testing the friend list request, we also want to check that our view is in the correct state when the request fails. That is why we’ll write two different tests here. A case when the request is a success and also one that is failing. Actually, there’s also a third state. The server returns an empty list of friends, meaning that our user has no friends. Just kidding, meaning that the user has not uploaded any friend information yet. This case is so similar to getting a list that actually contains friend information, that I’ll leave it for you to figure it out from the code.
But first, let’s take a look at the successful test.
Unit testing RxSwift — Successful friend requests
Since we are using RxSwift, the first thing we’ll need to is to create a
DisposeBag. Next, we’ll create our
MockAppServerClient. Right after creation, we define the
getFriendResult to a success and also set the payload to a dummy friend object.
Friend.with() is a static function that we have defined only for our testing target which helps us to create a dummy friend:
Next, we’ll create the view model that we want to test. When creating it, we’ll give our mock networking client as a parameter. This technique is called dependency injection and it helps us to make our classes testable. In our view models initializer, we have defined the parameter like this:
So, if we don’t give the networking client as a parameter, we use the default version (that actually makes network requests) instead.
Sorry for the somewhat clustered explanation 😅, but let’s continue with the test function code. Next, we’ll call
viewModel.getFriends() and make sure that the cells are ready. Now that we have all the things set up, we need to somehow confirm that our view model is in the correct state after we have downloaded a list of friends. This is where the
XCTest framework shows its force. We’ll use
expectation to create a variable called
expectNormalCellCreated. The way that expectations work, is that they need to be fulfilled before a certain time (that we define in our test case) or the test is marked as failed. Here, we expect that in the input data provided, i.e. the first item in the array that
friendCellsPublishSubject contains, is indeed a normal cell.
We’ll make sure this is the case by subscribing to the
PublishSubject and checking inside the
onNext function that the first item is a normal cell. If the item passes that check, we’ll call
fulfill for the expectation variable. We use
XCTAssertTrue to check the content so that the test fails immediately in case of wrong input. In case our app has a lot of tests, this decreases the time it takes to run them. This way, we don't keep waiting for a failing test case. After the
subscribe() call, the last thing we need to do is to add the returned object to the disposeBag.
Phiiuuff, finally we have everything set up. Now we only need to define a time limit in which we need to fulfill the expectation. Again, we’ll use a function from the
wait takes an array of expectations as a parameter (all of these need to be fulfilled) and also a time limit in which those need to be fulfilled. Since the
getFriends call in our mock class doesn’t make any network requests, as it is actually synchronous, we can define the timeout to 0.1 seconds.
Now, when we hit the run button, we’ll see that our test is passing. Next, let’s define the failing case!
Unit testing RxSwift — Failing friend requests
Our table view in the friend view displays an error cell when an error occurs. This time, we want to test that
friendCells contains an error cell:
The test case is very similar to the one that we just went through. But this time, the
getFriendsResult is defined as a failure. Also, the if-case statement now checks that there indeed is a
.error cell inside the array. The rest of the code is identical to the case we just went through, so you can figure it out on your own.
There are a lot more tests inside the
FriendsTableViewViewModelTests class but all of them follow the same pattern as defined in these two cases. Check them out and if you have any problems, questions or comments, just DM me on Twitter or comment below and I’ll get back to you :).
Now, let’s check how we can validate the user input data when unit testing an RxSwift app.
Validate input data when unit testing RxSwift apps
FriendViewModel is the type that is responsible for adding and editing friends.
FriendViewController is the view that draws the UI. Depending on what we are doing, we’ll either pass
UpdateFriendViewModel when opening the view. Both of them conform to the
FriendViewModel protocol. Today, we’ll look into the
AddFriendViewModel, which is used to add friend information to the server and see how we can write tests for it.
Testing the AddFriendViewModel
To send friend information, all the fields (first name, last name, and phone number) need to be filled. First, we’ll check that field validation is working. We prevent the user from sending invalid information by disabling the submit button unless she has provided valid data. We can test this by subscribing to an
submitButtonEnabled. It emits an event whenever data is changed and we can subscribe to it to check the state. Inside the
AddFriendViewModelTests, we have a test called
validateInputSuccess which does all this.
The code looks very familiar. We create
mockAppServerClient variables. Then, we’ll create a
viewModel and use dependency injection so that our mock network client is used. The mock network client, defined for
AddFriendViewModelTests, is so similar to the one that we just went through, that you can figure it out straight from the code.
Next, we’ll set the data for the user inputs: first name, last name, and phone number. After that, we once again create an
expectation, this time for a submit button state change. Then, we subscribe to the event, and fulfill the expectation only after the state emitted is true. In the last line of the test, we use the
wait and wait for the expectation to be fulfilled. And that's it! Now we have tested that our data validation is working.
Now let’s check out the last test that we’ll go through. A successful case of adding a new friend.
Unit testing RxSwift — Testing successful friend creation
Now, this is a familiar drill to us. DisposeBag, mocking a network, creating a view model, setting the input data and setting up an expectation. After we have successfully created a friend, we’ll navigate back to the friend list view. We know that the navigation is done after the
onNavigateBack is emitted. So, to verify that our data is passed to the server, we just make sure the expectation is fulfilled only after the
onNavigateBack is called. After that, we’ll call
submitButtonTapped to start sending the information to the server. Normally this is done by the user who pushes the submit button in the UI. If the server response is what we expected, which it will be since we just defined it to be what we want, eventually
onNavigateBack is emitted. Now when we run the test, we’ll see that it passes.
You can also find
testAddFriendFailure cases in the
AddFriendViewModelTests class. We won’t go through that since we already have all the information we need to implement the test. In case there is something that you don’t understand, please ask and I’ll explain it to you.
That is all that I wanted to go through today! We learned how we can test view models when using RxSwift in a project. We also learned how to mock network layers and use dependency injection for our tests. All this is pretty simple after you go through it once, but the first time might be a bit difficult. I hope I was able to help you and I hope to see you here again! Now, thanks for reading and have a great day my friend!
Swifty Sw Developer