For iOS projects at Elements, we use Uber’s RIBs architecture. RIBs stands for Router — Interactor — Builder. RIBs is built with (among others) testing in mind: individual RIBs classes have their own responsibilities and are isolated. On top of that, each RIBs class conforms to its own protocol, which allows easy mocking.
This article is not aimed at those who have not worked with RIBs ever before. If you are not familiar with RIBs yet, I recommend going through this article first, before reading further.
Let’s get started!
There is already a lot said and written about why you should or why you should not write unit tests. To not advocate for either of those, here are some (personal) reasons why I do like and write unit tests.
Imagine a scenario where we have a Root, which decides whether a user is logged in or not.
Your folder structure might look something like this:
The LoggedIn and LoggedOut RIBs are in these scenarios viewless since they will in turn decide where to route the user, based on business requirements.
We have a service, which helps us decide if the user is logged in or not:
In our RootRouting, we want to have the options to either route to LoggedIn, or route to LoggedOut:
Hooray! Now we can call these functions from the Interactor, and the Interactor knows everything about the user being logged in or not, with help from the UserServicing:
For the UserServicing protocol we use initializer injection, this will allow us to use a mock later in the tests.
Using the Builders for LoggedIn and LoggedOut, your RootRouter might look something like this:
Now we can start testing if the Router and Interactor are working as expected.
Don’t worry, I am lazy too. Other developers already got us covered. As you know, with RIBs we mostly use protocols, and we use dependency injection, the main ingredients for making unit testing easier.
What is left before we can start writing our tests, is writing the mock implementations of our protocols, and nobody likes the tedious work of writing mocks. But fear not, there is an excellent code generator for Swift, called Sourcery. While Sourcery can be applied to many domains, such as Codable, Equatable, and JSON coding, we are gonna focus on automatically generating mocks.
Sourcery writes code based on templates written in templating languages, in our case, Stencil is used. To be more specific, we are gonna use the AutoMockable.stencil template.
First, you have to install Sourcery as a dependency of your project. I choose to use Cocoapods, but several other options are available.
Add the following to your Podfile:
pod ‘Sourcery’
Now run:
pod install
Sourcery is now installed, and ready to be configured.
Next, we need to tell Sourcery which template to use, and where this is located. Also, we need to inform Sourcery about where to write the generated code.
In Finder, and not in Xcode’s Project Navigator, in your project folder, create a new folder named Templates, within this folder, create a new folder named Sourcery. Within this folder, add the AutoMockable.stencil. In our case, Sourcery will read from this template.
In your Test target in Xcode, add a folder called AutoMockable, within this folder, add an empty Swift file called AutoMockable.generated.swift. Make sure to select your test target for this file. This is where all the generated code will be placed.
In Xcode, select your project in the Project Navigator, select your Target, and select Build Phases. Here, select New Run Script Phase.
Give this run script a descriptive name, I call it Sourcery AutoMockable. Next, add this script:
$PODS_ROOT/Sourcery/bin/sourcery — sources ./ — templates ./Templates/Sourcery — output ./UnitTests/AutoMockable
Make sure that you use the correct paths for the template and output, and that the folder names correspond. Also, since I am using Cocoapods, the location of Sourcery is $PODS_ROOT/Sourcery/bin/sourcery. If necessary, change to the location where you have Sourcery installed.
Try to build the app, and check if no problems occur. Each time you build the project, Sourcery will check and write all the necessary code.
In the AutoMockable.stencil, you can find this line:
{% for type in types.protocols where type.based.AutoMockable or type|annotated:”AutoMockable” %}{% if type.name != “AutoMockable” %}
This tells us that Sourcery will check each implementation of a protocol called AutoMockable, or an AutoMockable annotation in your project.
This gives you two options:
2. Or add the protocols you want to have mocked in between the following annotations. For example:
Choose which one suits you best, since they both give the same results. To test this, make RootRouting conform to, or annotate as AutoMockable
Build the project, and open AutoMockable.generated.swift. You will probably see some errors, such as:
This is because AutoMockable.generated.swift has your test target as its target membership, so you have to import your project's main target.
On top of this, you need to import your dependencies as well. While it’s generally good practice to not have to import too many third-party dependencies, sometimes you have to. In this case, when using RIBs, we need to import RIBs and RxSwift, since RIBs has a dependency on RxSwift.
Unfortunately, you can not simply import anything in the AutoMockable.generated.swift file, since this file will be written each time you run or build. However, you can edit the AutoMockable.stencil, from which Sourcery reads what code to generate.
Open the stencil, in my case located at MyProject/Templates/Sourcery, with your editor of choice.
As you can see, you are able to import your dependencies. Here I added:
@testable import MyProject
import RIBs
import RxSwift
Save this file, and build your project again. When you have imported all your dependencies, you should be good to go.
Now this setup is out of the way, we can finally start adding our unit tests. Remember that the business logic is embedded in the Interactor and Router, so let write tests for those.
Start by adding a new file, named RootRouterTests to your test target. Add your subject under test as a variable on top. I like to call this sut.
When we want to initialize this RootRouter, we need mocks.
Looking at the initializer of RootRouter, we need mocks for:
Make these protocols conform to AutoMockable, or add them to the AutoMockable annotations, and let Sourcery do its magic by building the project.
Check your Automockable.generated.swift file, and see if the mocks are created. For each protocol, you should have something that looks like this:
As you can see, Sourcery created a mock for us, conforming to the specified protocol. LoggedInBuildable in this case. It has created the implementation for us, and some variables to configure our mock, and to assert some values.
Now we have created the mocks for the RootRouter, we can set up the test.
Let’s start with writing a test that will verify the behaviour of routeToLoggedIn. When we call this method, the LoggedInBuilder’s build method should be invoked, this should return the LoggedInRouter and attach this LoggedInRouter.
The implementation of the RootRouter should call the build method of the LoggedInBuilder. We do not want to have the builder’s logic in our mocks, so we can use the closure Sourcery already provided for us. This has to be done before running the test, under given.
If you need mocks for this setup, such as LoggedInInteractableMock, simply make them conform to AutoMockable, or annotate them again.
Now we are sure that the build method on LoggedInBuilder returns a mock LoggedInRouting, and we have references to test against, we can write the actual assertion.
As you can see in this test, when sut.routeToLoggedIn() , then we assert that load() on the LoggedInRouter is called and that build() on the LoggedInBuilder is called. Both methods are invoked in the mocks and loadCalled and buildWithListenerCalled are set accordingly. Again, this magic in the mocks is automatically generated for us by Sourcery.
Run the test, to check if the test succeeds or fails.
Now, let’s do the same for routeToLoggedOut. This should be fairly easy now since we know how to set up the test, and the behaviour is more or less the same. We just need to make sure we invoke the methods on the LoggedOutBuilder and LoggedOutRouter mocks.
Again, make sure you let Sourcery create the necessary mocks.
Hooray, we have covered the RootRouter with unit tests!
What is left now is adding unit tests for the RootInteractor. Start by adding a new Swift file to your test target, and name it RootInteractorTests. Add the subject under test as a variable on top:
Looking at the initializer of the RootInteractor, we need mocks for:
Let Sourcery create the necessary mocks, by adding them to the annotations, or conforming to the AutoMockable protocol.
Now you can set up your test case.
The RootRouter is not assigned in the RootInteractor’s initializer, but rather the other way around. Make sure we give the RootInteractor a mock RootRouter to work with:
In the RootInteractor we want to test if the UserService calls userIsLoggedIn() on didBecomeActive() and we want to test if the correct methods on the RootRouter are invoked, given the results of userIsLoggedIn()
userIsLoggedIn() in the UserService will return a simple Bool. In the first test, this return value is not important, we just want to see if the correct method is invoked. We can set up this return value in a simpler way, using a closure is not necessary.
Instead of using the given closure, we use userIsLoggedInReturnValue
Again, for this specific test, we do not care about this value, but we need to set this value to prevent the test from crashing.
Let’s write the rest of the test:
Run this test to validate if everything is working correctly.
Next, we want to test if the correct methods on the RootRouter are invoked, based on the return value of the UserService. If false, we want to invoke routeToLoggedOut and if true, we want to invoke routeToLoggedIn .
Using the same userIsLoggedInReturnValue on UserService, this should be fairly easy. Now, this value does matter.
Here we test if we invoke the correct methods based on the return value userIsLoggedInReturnValue
Run the tests to validate if everything is working correctly.
We have now covered all the use cases in the RootRouter and RootInteractor!
Although these examples were fairly easy, it shows how useful automatically mocking your protocols can be. Instead of having the tedious and time-consuming task of writing our mocks, which is most of the time boilerplate, we can now focus on our tests.
If you get used to making your newly created protocols conform to AutoMockable, or annotate them, right when you start on a new feature, it gets even easier.
There are arguments against automatic code generation, and I get that. There will probably be mocks generated which you will not use, or not completely use. However, if you make sure the generated code is in your test target, you do not have to worry about not covered code in your production application.
Sourcery does have its limitations, for instance, generics and overload functions are not supported. However, these are both workable, and easy to avoid.
I wrote this article with the help of these amazing articles and tutorials, and I highly recommend checking them out.
[1] https://medium.com/swlh/ios-architecture-exploring-ribs-3db765284fd8
[2] https://github.com/uber/RIBs/tree/master/ios/tutorials
[3] https://oozou.com/blog/generating-mock-classes-for-unit-testing-in-swift-48
[4] https://www.caseyliss.com/2017/3/31/the-magic-of-sourcery
This blog was written by
Marco
on
Feb 15, 2021