Mastering the Art of Unit Testing: iOS
Unit Testing is one of those things in App Development which we feel are redundant, or at least a burden. But once you understand the importance and ease of writing Unit Tests, you are surely never going to turn your back on it again.
Developers are usually confused between Unit Testing and Test Driven Development (TDD), although these two terms are not exactly synonymous. TDD is an approach to writing better code.
If you are already familiar with Unit Testing, you should consider finding more about TDD after reading this post.
In this blog, we would be discussing the following :
- What is Unit Testing and why should we integrate it in our codebase?
- What are the different types of Unit Tests and how to write them?
- What is Dependency Injection and how to leverage it to write Unit Tests?
- How to go about writing Unit Tests for asynchronous tasks like Networking?
What is Unit Testing?
“Unit testing is a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinised for proper operation. Unit testing can be done manually but is often automated”
The above statement is a textbook definition of Unit Testing but there is one word that needs special emphasis: ‘Unit’. A Unit can be a function or a variable exposed in the public interface of a module. In short, Unit Testing is a way of asserting the actual behaviour of the unit with it’s expected behaviour.
Why Unit Testing?
One of the most common response I get when I ask a developer to start writing Unit Tests cases is, “Is there less on our plate already? ”, “There are Quality Assurance Engineers in the company to verify if the product is working as per requirement, why do I need to spend so much time to write Unit Tests?” and so forth.
I agree, there are many QA engineers in place in every company and almost all the developers are already occupied with lots of work but, haven’t you ever faced an issue where you refactored something and somehow it got missed by QA( they are human too), and there is a Production issue. With the help of Unit Testing, if you have written Unit Tests covering all the possible business use-cases, these things will not happen.
There are couple of reasons why we should be adding Unit Tests in our codebase.
- Verify if your code does what you expect Whenever you say that the product feature you have developed is working as per the requirement, it is an intuition you have. Can you for sure claim the above statement with 100% surety? Even if you can, you do not have a proof of your statement. Unit Testing helps to provide the proof for the above claim.
- Makes Refactoring much less painful Many a times, developers refactor code as and when it becomes complex and unmanageable. Haven’t you even, in lieu of refactoring, break your business logic? I agree, there are many QA engineers at place and the developers are already occupied with loads of work but, before even shipping it to Production, your Unit Tests would indicate that your code has been broken.
- Forces you to write smaller, more concise methods When you start writing Unit Tests for your applications, you will come to realize that it enforces you to write smaller and concise methods following SOLID principles.
- Avoid Code Smell — When did code smell occur? Code smells : “Smells are certain structures in the code that indicate violation of fundamental design principles and negatively impact design quality” These are not bugs, but these things slow your development or are an indication of a weakness in design. Code smells are very subjective, and varies from developer to developer and team to team. More often, developers do not understand when it started but once you start integrating unit test cases with your CI system, you will figure out exactly when it did start.
- Provides executable instructions and documentation Let’s say you have a new member in the team, to understand the product properly, generally there is no documentation of the actual requirements and it’s correlation with different modules present in your codebase.
Unit Testing: Conventions
There are certain conventions we follow while writing Unit Tests.
- All Unit Test methods must begin with the word test
- Unit Test methods must have a return type of void
- Unit Test methods cannot have parameters
- Unit Test classes are subclasses of XCTestCase
- Can have as many Unit Test classes/files as needed in a project
- To run some code prior to each Unit Test, override the
- To run some code after each Unit Test, override the
- Unit Tests are written for public interface of a particular class.
You cannot write Unit Test for private methods. Ideally, if you are testing your public interface properly for all the possible use-cases, you need not worry about the private methods. As, they are automatically verified once public interface clears all possible use-cases
How to go about writing Unit Tests?
We have discussed what exactly is Unit Testing, why should we do it and what are the conventions we should know while writing Unit Tests(UTs). But, still we haven’t seen how to integrate them in our applications, and how to start writing Unit Tests?
Writing Unit Tests is a three step process.
First of all, you should write a Unit Test, and ideally it should fail as, ideally, there is no production code written against it. Then you should write the bare-enough code in the production target to make sure the test succeeds. It could even mean, if you expect an outcome as 1 from a method just return 1 as a hard-coded value to make sure the tests pass. Now, you can start writing your business logic in that particular method and run the tests in parallel. This will go on as an iterative process, until you have incorporated the complete business logic and the tests are not failing.
Types of Unit Tests
Broadly speaking, there are three types of Unit Tests. We identity these different types of Unit Tests based on the type of unit we are focussing while we are writing Unit Test. Whenever you are writing unit-tests, you can easily distinguish it as one of the following Unit Tests:
- Return Value Test
- State Test
- Interaction Test
We will see in detail in each of these types, to see how to write Unit Tests in these cases. While writing a UT, we follow AAA principle( Arrange — Act — Assert). Which means, we setup the system so as to be able to trigger the particular interface, then we trigger the function and finally assert in a way that we can make sure the interface is behaving as per requirement. In each of the following type of Unit Tests, we will see how to go about implementing AAA principle.
Return Value Test
This is the use-case when our System Under Test, more commonly referred to as SUT in Testing terminology, is an interface which returns a definite value on a trigger. In this case, we setup the object(ARRANGE) , we call the method that returns a definite value(ACT) and then we compare the result with the expected outcome(ASSERT).
Let’s understand this with the help of an example. Let’s say we have a very basic class
Calculatorwhich has a method
add which returns the sum of two objects.
Now, when we write Unit Test for this class, the Unit Test class will look something like this.
If you notice in the
setup() of the XCTestCase, we have initialised our class
Calculator. Then we defined as test function called
test_add() following the convention. There we trigger the function add and then assert the outcome to the expected result. I would also like to point out, we destroyed the instance in
tearDown()which we created in
setup(). This is generally recommended to destroy any instance we might have created while setting up for a test.
But, why did we explicitly initialised in
setup() and destroy in
tearDown()? This has to do with the lifecycle of a test function. For every test function that is run, first
setup()is called, then the test function is triggered, once it exits this function,
tearDown()is called. So, in case you have two or more test functions, for every test function ,
setup() is called prior to running the test function and after that once function has done its job,
tearDown() is triggered. It is recommended to destroy any instance which we might have used while arranging the system or while acting upon it, because we would not want any unknown changes affecting our test, which might have been there because of some other test which was run previously. You would be able to appreciate this once we start having singletons and other such classes in our scope of writing test cases.
Now, more than often, you will observe that many of our functions do return a definite value and in turn they change some internal state of the object. So, we would not be able to write UT for such a method like we did for Return Value Test.
In this case, we setup the object(ARRANGE) , we call the method(ACT). Now, since we don’t have a definite outcome to compare, but we know this method must have changed some internal state of the system. We query the object in such a way, that ensures us to compare the state of the object against the expected state(ASSERT).
Let’s look into this with the help of an example. Here, we have a class
UserReward which lets us know the membership type of the user, based on the number of the points the user has.
Now, when we write Unit Test for this class, the Unit Test class will look something like this.
If you notice, we have four different test cases, for different use-cases. And, you can see how I have named all my four different test functions
test_<sut-name>_<use-case>. It is always recommended to write Unit Tests for failure cases as well. Now triggering the function , we query the object for its
membershipType to ensure the
updatePoints(with:_) updates the
membershipType to the correct value. And, also note, how we are setting up the object in
setup() and destroying the instance in
tearDown() as discussed earlier.
The previous two types of Unit Tests are fairly straight-forward and easy to implement. But, there are use-cases which won’t be covered by the above two types. Let’s say you want to write a Unit Test for an object which interacts with your DB, or Network layer or a singleton for that matter.
These cases can’t be covered by the above ways. Even if you wish to do so, you might touch certain things which were meant only for production code. Let’s say you have some objects stored in
UserDefaults which you want to test. By using above ways, you might end up altering it even for Production code such that we might end up global states for the application. This would violate the principle of Unit Testing.
Unit Tests should be independent and concise.
Let’s understand this with an example. When you go to a restaurant to have a meal, the waiter takes your order and passes it onto a cook who prepares the order and then the waiter delivers it to you. In the entire process, you, as a customer, are not aware who the cook is and how did the cook prepare the dish for you.
If you want to write tests on the Waiter, you see Waiter has a dependency on Cook. If we were to write Unit Tests just as we did previously, we end up actually creating an instance of a real cook, which will in turn affect global states. So, what we should do is make use of a fake Cook.
Why do we need to mock objects?
From our previous discussion, we came to a conclusion that we need to mock objects. But why to do so? There are couple of reasons why we should mock objects. Firstly, using real objects would end up consuming time and resources which in process would make our UT slow and that would be last thing we want. Unit Tests should be fast enough so that we should be able to run them in a matter of few seconds. Else, no one would run tests on every change if it is time-consuming. Secondly, not making a fake object, might end up tweaking our global states of the application which we do not intend to do.
How to go about making mock objects and using it to resolve our issue at hand. First, let’s see how to make Cook as explicit dependency and let see how it will help in resolving the issue at hand.
Taking the previous example forward, let’s understand how making a Fake Cook works with the Waiter class.
What we are intending to do is creating an interface, so that in testing environment, Waiter uses Fake Cook and in production environment, we use actual Cook. This is achievable in swift by creating a protocol which will abstract out all the functionalities of a Cook. And, then Waiter can use the appropriate Cook using a principle called Dependency Injection.
Before going any forward, let’s see what is Dependency Injection and how to leverage it to resolve the above use-case.
Dependency Injection is a 25-dollar term for a 5-cent concept
To be very honest, the above quote is very appropriate. This concept seems to be very tough by the name of it, but when we go about using it, we will find out that it is comparatively easy. Some of you might already be using it in your applications without explicitly calling it out. Dependency Injection(DI) can be done using following ways:
- Extract and override
- Method Injection
- Property Injection
- Constructor Injection
Lets see each one of them in a bit more detail.
Extract and override
Here we have a simple class with a function which returns a value based on a number stored in
UserDefaults. Note that
getNextID() has a dependency of
UserDefaults which it is using on its own. This will make it difficult to write Unit Tests in this state. We wish to modify this in such a way that we can use a
MockUserDefaults while writing tests and actual
UserDefaults while running production code.
So, what we can do is, extract the part of the function as a separate method which we can later override in testing environment.
In testing environment, we can use Mock Class as shown below.
Now when we need to write Unit Tests for this class, we can easily do so by using the types of Unit Testing we have seen before.
Note, how easily we have now overridden the part which was stopping us from writing Unit Tests and now we can easily assert on the outcome because we know what the
UserDefaults in testing environment will return.
Here again we are considering the same example as before. We have already seen how to inject the dependency of
UserDefaults in this class using ‘extract and override’. Now, let’s see how to go about doing so using Method injection. Here, rather than overriding some methods, we would simply want to pass on the dependency in the function we are interested in.
You can see above, how instead of separate function, we have passed
UserDefaults as part of the function declaration. Now, in testing environment, we can simply pass on
MockUserDefaults object instead of an actual
Please note the difference in injecting
UserDefaults in this way as opposed to the previous way. Here, we are injecting the
MockUserDefaults while triggering the function. This also help us to write tests on the actual class rather than a subclass of the actual class, overriding some methods.
Sometimes, we know what would be default values of some of the dependencies of a particular object like
UserDefaults or Location Manager. In such cases , we can also make the dependencies as an optional property on the object and assign it post initialisation.
This is one of the most commonly used ways to inject dependencies to a particular class. In this way, we inject all the possible dependency to a particular object while initialising it. This helps in explicitly knowing all the dependencies, a particular object has. More than often, you would be using this way of Dependency Injection.
Please note the main difference in this approach. We are injecting the dependency at the time of setup as opposed to previous approaches.
Types of Stub
Till now, we have seen how dependencies of our SUT can be injected. To write Unit Tests on such tests, we require to make fake dependencies. There are two types of fakes we can make: Stub and Mocks.
The main difference in the above two types of fakes is which way is the test pointing. While using stubs, SUT is triggering the fake SUT and receives a pre-canned result from it. This in turn changes the state of SUT, on which the assertions are made. While using Mocks, SUT is communicating with the fake object, but the tests runs around the dependency and asserts on the input it receives from the SUT. We can have too many stubs, but only one mock at a time while dealing with SUT.
Now that we have seen different ways of using Dependency Injection and the types of Fakes, lets again focus on the problem we left in the middle. How to write tests for the waiter.
First, lets abstract Cook into a protocol to be able to make a Fake out of it.
Next, let’s tweak Cook to conform to the above protocol.
Now, let’s tweak Waiter to inject Cook using Constructor injection, rather than using it as before.
Now we can easily write Unit Tests on Waiter. Wait, we forgot to make the Fake Cook. Let’s do it now.
Now, we can easily inject the FakeCook while setting up the object.
Unit Tests : Networking
Till now, we have examined various use-cases to write Unit Tests. But, still there is one very common use-case which we all face in all the possible apps we make — Networking. These are asynchronous methods, and it’s always confusing how to write Unit Tests for such cases. And, note the principle of Unit Testing which is — it should be fast and should not touch real resources. That means, we should not actually make network calls while testing. So, we need to stub the responses of our network calls.
Having said all of this, we can still write Unit Testing based on the concepts we have seen so far. Let’s explore further with an example.
Mostly, we make network calls in our ViewModels (or equivalent layer based on the app design pattern used). I am assuming MVVM for now. And there is one function which makes a network call to fetch a list of items. In the network layer, for the simplicity, I have only considered one input- url and a success & failure completion block.
We need to write Unit Tests on our
ViewModel. If you observe the
ViewModel class, the bottleneck for writing Unit Tests is
NetworkService which is not injected. Also, there is no means to create a mock
NetworkService to ensure we do not make use of actual
NetworkService in testing environment.
Let’s apply our learnings so far. Firstly, let’s abstract
NetworkService into a protocol and let
NetworkService conform it.
Using Constructor injection, we should now inject
We are now left with one thing before actually writing Unit Tests for
ViewModel — we should make a
Now let’s see how to write Unit Testing for
ViewModel after making all the changes.
Please note, how I have passed the expected response as part of the
NetworkService completion block for success and failure respectively. This way, we are in full control as in what kinds of responses we are expecting. This enables us to test for invalid response from backend, corner cases and error scenarios.
At last, I hope you could now appreciate the benefits of Unit Testing and how to go about integrating it in your codebase. In case, you want to read in-depth about Unit Testing, I recommend the following resources: