Practical unit testing for iOS applications

Practical unit testing for iOS applications

Sep 21, 2017 • 5 min read

Unit tests are the de facto standard for establishing a consistent product and an efficient continuous delivery process. They force you to write well-structured code that is split into modules with clear interfaces between them. Unit testing is necessary to safely refactor your code. In this post, we will discuss the process we used during a customer project that can be used to make even the most complex codebase unit testable.  

Understanding unit testing

Before we dive in let's review the basics of unit testing. According to Martin Fowlerone of the best specialists in refactoring, unit tests are:

  • low-level, focusing on a small part of the system (one unit)
  • usually written by the developers themselves
  • faster than other types of tests — and therefore should be run more often

A customer’s testing problem — and its solution

Our customer, which we’re calling “X” here, is an e-commerce organization that asked us to help with a mobile project which had no unit test coverage. They were in the process of moving to a new version of their authorization API, but without unit tests it was quite hard to achieve because changes were causing bugs in other components. The few tests they had weren’t proper unit tests, since they depended on networking logic and external services. It was also hard to test some of the functionality because their code hadn’t been divided into small, isolated, robust parts. Plus, the project contained some reactive code in the service layer.

This is a common problem for all customers like X — and this is why it hurts

This is a classic issue for large enterprise e-commerce projects. It’s really hard to fight against complexity in huge code bases. Usually, e-commerce mobile projects require sophisticated deep linking, analytics, and a huge networking layer with an API originally prepared for non-mobile projects. The number of features to be implemented is extremely high for every iteration, so technical “debt” grows and the app’s stability keeps getting worse over time. But there are ways to reduce complexity and make your project unit-testable. Here are some best practices to follow:

  • Isolated code
  • Properly-configured build file and project file
  • Small tested units
  • Meaningful assertions
  • HTTP request stubs
  • Self sufficiency
  • Modularity
  • Test all clauses and test cases

This is how we solved X’s problem

At first we prepared a plan to refactor the project and cover it with unit tests. Since the team that worked on the project before us was not very experienced in unit testing, we prepared tutorials and presentations to show them the best unit testing practices. Then we started refactoring the code, step by step, with small iterative changes that didn’t materially affect the development process.

Stubbed network requests and API calls

We advised them to stub all network requests for unit tests, which we did using OCMock and OHHTTPStubs. For unit testing, using real data like IDs, passwords, user data, and so on is not a reliable approach; once the data is changed and becomes outdated, your tests fail even though your logic is still correct. Unit tests are supposed to test only internal app logic independent of other services, backend and real data; for these purposes you can use Integration Tests or Functional Testing, also called E2E integration testing.

Decreased coupling

Isolated token validation logic

We figured out that the main problem occurred in the token validation logic, which contained many lines of static methods in the single class and was coupled with lots of services. So we isolated that logic and covered it with unit tests to be sure it didn’t have problems in the future.

Visualization of before and after effort to make code base unit testable

Security storage

Also, token expiration problems were being caused by incorrect storing of the token. We extracted the storage logic into a new module to isolate it and make it testable.

Dependency inversion

Injections make your code more testable because they allow you to test only that part of the code you need to test. For example, you can inject networking services in services which use them. Now you can pass a fake networking service with stubbed network requests to the services you would like to test, i.e. some parsing logic or request processing. This becomes even more useful for languages like Swift which don’t have many runtime hooks for mocking the objects.

a visual of a mock injection for unit testing purposes

Writing highly maintainable unit tests

Tested units

A unit test should cover behavior of one unit of code, and its name should be meaningful enough that it helps find what is broken.

Test all clauses and edge cases

Ideally, you need to be sure that all clauses (if, else, else if, switch) of your logic are being executed during the test run. If you assume that your parameters have some edges, i.e. mix and max values, you need to pass them as well.

Code style

All practices for writing good, maintainable code are applicable for unit tests as well. Think DRY, SOLID and so on. No magic numbers, no copy/paste. You will need to maintain these tests and write new ones as needed.

VIPER

VIPER is an architecture dedicated to making your application unit testable. There are many posts that describe VIPER. Here are a few of them:

Continuous Integration

Of course, to make unit tests useful you need to run them, and you need to run them on every code change attempt. That is where Continuous Integration helps. We established a CI process which runs unit tests on presubmit. That means the change list which hasn’t passed its unit tests cannot be pushed into the repository. We also made it mandatory to run unit tests on every part of the service and models code in order to pass a code review. For this step it would be cool to have a tool for static analysis and collecting the metrics of your code, perhaps something like Sonar, which is not available as part of the iOS CI infrastructure as of this writing.

Conclusion 

Here’s the business benefit our client got from solving the problem mentioned above

After refactoring and running unit tests, the app became much more stable. Indeed, we were able to refactor two key business features in the app without any major defects, and this significantly increased user involvement. We have made it possible to cover the project with tests and run CI/CD both quickly and easily so we can always be sure the app is stable and that we’ll see red flags if unit tests are not passed. This new architecture means we can ship new features faster — and without risk of adding new bugs despite our increase in development speed.

Suggested Reading

We’ll start with “classic” texts. You’re probably already familiar with them:
1. Test Driven Development: By Example: Kent Beck - https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530
2. Refactoring: Martin Fowler - http://www.martinfowler.com/books/refactoring.html
3. Code complete: Mcconnell (it’s about code in general) - https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670

Some links dedicated to iOS unit testing:
1. Quality Coding
2. objc - issue 15 - testing
3. Test Driven Development
4. Unit Testing, by Matt Thompson
5. iOS Unit Testing

iOS Unit testing books:
1. Test-Driven iOS Development: Graham Lee 
2. xUnit Test Patterns: Refactoring Test Code: Gerard Meszaros

Subscribe to our latest Insights

Subscribe to our latest Insights