Become a Unit Test Master

Some tips and concepts to elevate your unit testing skills in iOS Development.

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.

Basic concepts

Best Practices

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.

Libraries

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

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 setUp and 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 XCTestCase.)

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:

Given-When-Then (GWT)

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.

Arrange-Act-Assert (AAA)

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

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

Dummy

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.

Fake

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.

Stub

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.

Spy

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:

Mock

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.

Fixtures

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 nil, empty or 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

Reflection is a common programming language feature that enables us to examine and modify the behavior of methods, classes, and interfaces at runtime.

Swift’s version of reflection enables us to iterate over, and read the values of, all the stored properties that a type has — whether that’s a struct, a class, or any other type

Mirror

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 name and 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.

Async Tests

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:

Code Coverage

Last but not least, we're going to talk about code coverage. You can enable this feature in your Xcode following the steps below:

  1. Tap in your app scheme and select Edit Scheme...
  2. Choose the Test option on the left side menu
  3. Select the Options tab
  4. Look for Code Coverage option and mark the checkbox, with all targets selected

Try to run your tests. You can now check how much of your project was covered:

  1. Go to the Report navigator item and select the Coverage option

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.

Conclusion

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 :)

References

iOS Software Engineer @ iFood — Programming, UI Motion, and cats.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store