When we talk about testing software, we can clearly remember the Testing Pyramid. Today we're going to talk about the cheapest and the easiest type of tests: the Unit Tests.
What is Unit Testing?
Unit testing is a way of testing a unit of some software component. By unit, I mean the smallest piece of code that can be logically isolated or a set of objects interacting with each other. It can be a function, a method, or a property.
Why we test?
Unit testing means delivering more quality code to our users. You may think "but our users can't see the code…" and you're right, but they see the final result of your hard work, and if you write unit tests, it ensures that your code was tested and a lot of scenarios that would cause bugs that lead to a bad user experience was covered.
When we say “delivering quality code” we’re saying that:
- Tests help us make changes in our code;
- Bugs can be found earlier;
- Testing saves time and money;
- Forces you to think through your code and make it more testable.
These are some important reasons why we test, and I will try to make you understand them all by the end of this article.
How do we test?
To start testing, you need to make your code testable.
The easiest way to achieve this goal is by making your class depend on abstractions, not concrete objects. That way, you can manipulate all the objects using what we call test doubles. You can use test doubles to mimic real objects and make them do what you want. We're gonna see more about this later in this article.
Another tip is to avoid using static methods and singletons. They can be tested but you'll see how hard it is comparing to injected abstractions.
Developing your views using
ViewCode instead of .xib or storyboards is really helpful too. Since it's easier to initialize; manipulate its dependencies and test its subviews when testing.
What do we test?
Pretty much everything is testable, but you shouldn't rely on getting 100% code coverage, you should think about what is important to be tested, like what your code does. Below I listed some things I consider important to be tested:
- Public methods: you should never test private methods/properties. These things are there to give an output, and you'll test all the scenarios starting from the public method;
- Architecture cycle: choosing an easily testable architecture will make your tests a lot easier, and testing your architecture cycle will help you guarantee that all classes and layers are working properly;
- Business-oriented layout: sometimes we need to make layout changes due to some business rules. You can test if your class components behave as expected when these rule changes.
TDD vs. BDD
These are acronyms to some techniques used for tests.
- Test-Driven Development (TDD): It's when you write your tests before the functionality. Because of this, the tests are going to fail. Your job is to write the simplest code that can make the test pass and with this, you can start refactoring. This process is called Red, Green, Refactor;
- Behavior-Driven Development (BDD): It's the process of writing and running tests with a prior understanding of user behavior. Unlike TDD, BDD generally begins by developing the system's behavior. The main advantage is the common language used to write tests because it can be understood by technical and non-technical teams.
Before we start with code, here are some best practices for writing tests:
- Tests should be independent: your test should run in random order without failing and cannot produce side effects for other tests;
- It should be fast: avoid writing tests that need to wait for things to happen;
- Completely isolated: it should not rely on external systems (APIs, network calls, databases, etc) or things without your control, like environmental values (language, the current timezone, etc);
- Make it descriptive: the test's name should be enough to understand what's being tested.
Since this article is about testing in iOS, I'm going to use XCTest. It's a native library for creating and running tests, UI tests, and performance tests. You can search for more frameworks like Quick and Nimble.
SUT is how we call what subject we’re testing, it’s called Subject Under Test.
setUp( ) and tearDown( )
- The setUp method will be called before each test. Use it to set up your SUT.
- The tearDown method will be called after each test. Use it to perform a cleanup.
You don't really need setUp and tearDown… 🤔
According to Apple's Documentation, the
tearDown methods are not required.
In the default case when run tests, XCTest finds all the test classes and, for each class, runs all of its test methods. (All test classes inherit from
For each class, testing starts by running the class setup method. For each test method, a new instance of the class is allocated and its instance setup method executed. After that it runs the test method, and after that the instance teardown method. This sequence repeats for all the test methods in the class.
So, you can do like this:
This style/template is part of BDD (Behavior-driven-development), and we use it to make the way we describe some functionality more readable.
GIVEN: it's when we define the starting data and set up our objects;
WHEN: it's the key action; we need to call the method under test with the given parameters;
THEN: here we verify the system output and see if behaves expectedly.
We can read it like this: given a context, when some condition happens, then expect some output.
Almost like GWT, this pattern is also used to facilitate our test readability and suggests that you divide your test into three sections:
ARRANGE: it's when you set up your objects and set some data;
ACT: will invoke the method under test with the arranged parameters;
ASSERT: verify if the method under test behaves as expected;
These two patterns are pretty much the same and it's up to you to decide which one fits in your development.
For me, GTW opens up possibilities, since non-technical people (like business people) can understand due to its reading naturality.
Test doubles are test objects that are used to mimic the behavior of some real object. The difference between them is that you will define the responses and the scenarios you want the doubles to have.
"Test Double is a generic term for any case where you replace a production object for testing purposes."— Martin Fowler
Dummies are potato objects that are just used to satisfy method parameters and act as a placeholder. This means that you don't do asserts with these objects. They just… exist.
In the above example,
NetworkDummy is doing nothing more than acting as a placeholder.
Objects that have working implementation but with a reduced logic. For example, if you have a complex class performing a validation to save something in a local database, you don't need to recreate or performing all these validations again, you can fake it and just return the result you need.
You use stubs to control the outcome of some dependencies method. The example below shows that we can change the
fetchDataToBeReturned value when testing to make it return the scenarios we want to.
You can use it to inspect the properties of some dependency. The example below allows you to assert if the
fetchData method was called and what id was passed.
The properties should be using
private(set) access. This means that the property is public for getter, but private for setter. You can only read it from the outside, and this is what we want in our tests.
It's also very common to create a Spy that is also a Stub, so you can end up with a class like this:
As we usually see in a lot of articles, mocks arent classes that we just see the number of times a method was called.
Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive.
Since mocks are complex classes, the community always relies on Frameworks, like Mockito or jMock (both written in java), they're clean and ready to use. For Swift, we have a limited number of mock frameworks and they require some boilerplates because Swift doesn't have some of the features needed to make really worth using.
For that reason, when we write our doubles we usually use Stub or Spies.
Unit test fixtures vary based on context, programming language, or framework. In iOS Development, fixtures can be used as static functions that will contain all the necessary code to initialize some object. You'll use it instead of using the concrete class initializer within our test methods. Here's an example:
As you can see, we usually write the fixture method with
default values. This is going to make your tests understandable and cleaner because:
- you should explicitly write your initializer's parameter values;
- you ain't gonna need to write all the parameters every time, just the ones you are going to use, like this example:
The last benefit from fixtures is that when new dependencies are added to the class initializer, you just need to add them once within the fixture. You don't have that benefit when using the default class initializer, all the tests using it are going to fail and will need to be updated.
Reflection is a common programming language feature that enables us to examine and modify the behavior of methods, classes, and interfaces at runtime.
In Swift, the reflection is made available through the Mirror API. It's a standard library and the access to the objects we want is read-only.
In this example, we have a simple class
Person that contains
age properties. When we mirror it, we can iterate over the class children and access all properties that it
valueis of type
String, for example. In our case, if we had more String properties, they would be printed.
Of course, we don't want to cast every time we want to access some property. So, we can make an extension to return the first property matching the
type (String, Int, UILabel, etc) and the
label (your property name as String type) provided by method's parameters.
With this made, now we can access all the properties we want easily. In this example, I'm accessing the
name property that is of type String.
When doing tests for async calls, is common to use
XCTestExpectation followed by a call to
wait(for: XCTestExpectation, timeout: Int) and there is no problem using it, but if you make assertions inside the completion block, you'll see its impact in the future. See the example below:
What is wrong with it? When you perform multiple async assertions, some tests can interfere with each other. To prevent this, the assertions should never be made inside the completion block, but rather, the values to be asserted can be captured from the completion and asserted after the wait call, like this example:
Last but not least, we're going to talk about code coverage. You can enable this feature in your Xcode following the steps below:
- Tap in your app scheme and select
- Choose the
Testoption on the left side menu
- Select the
- Look for
Code Coverageoption and mark the checkbox, with
Try to run your tests. You can now check how much of your project was covered:
- Go to the
Reportnavigator item and select the
2. Xcode will show you a list with all your classes and the coverage percentage for each one.
The code coverage shows which part of your code was covered (in green), which part was not (in red) and which scenarios within a function were not called (with stripes).
You don't need to think about getting 100% code coverage, but about improving your code to make it more testable and testing only the important things. With this, you will see that 100% coverage is not easy because there's always something to improve, to change, and make your code even better.
This is the end of this article and you may have noticed that I left some topics behind, like how to test gestures, views with storyboard or .xib, and maybe a really deep knowledge about some of the topics listed in this article.
The real objective of this article was to guide you through some concepts about how we unit test, shown some techniques and a lot of benefits. To master unit tests, you need to practice and implement this culture in your projects every day.
If you have any feedback or suggestions, feel free to contact me on Linkedin
That's it! Hope you learned something new today. See you soon :)