You're working on a new feature and suddenly an old feature stops working even though you wrote unit tests. Or you're refactoring legacy code and you think it's done, but suddenly you find a lot of bugs. So you go back, you make the fixes, and you think it's done, but then you find more bugs. You repeat and you think it's done every time, but the same thing happens, every time.
You have to write tests to make sure that your product is working, and the only way to prove it's working is through testing. And you need to be testing constantly in order to catch regression errors so that you can add new features without breaking existing ones.
So you're working on software with tests and yet you have a bug in the logic that was already covered by those tests?
Your test results have given you a false positive about your software.
Test-Driven Development (TDD) is the best solution for this problem. TDD is not unit testing. Unit testing is writing code to test our code. So first, we write our code, and then write unit tests to verify our logic that we just wrote. But TDD is totally different. With TDD, you write the tests first before writing the code.
TDD forces you to see your failing tests, which shows you how to turn them into green when you fix these failed tests.
β
Test-driven development definition
TDD is a development technique where you must first write a test that fails, then write code to get it working, and finally you will need to refactor the code to be as simple as possible.
β
β
In many cases, writing automated tests is seen as not real work and boring compared to building new features. TDD, however, turns testing into a design activity. We use the tests we write to clarify our ideas about what we want the code to do. It also keeps code as simple as possible so itβs easier to understand and modify, especially since developers spend more time reading code than writing it.
As we develop in TDD, it gives us feedback about the quality of both its implementation (does it work) and design (is it well structured).
Each level of tests should answer specific questions about our code.
1. End-to-End Tests: Does the whole system work?
2. Integration Tests: Do our objects work with each other correctly?
3. Unit Tests: Do our objects do the right thing?
β
TDD process
First we need to write user stories, which describe the system in detail. User stories describe business features in a convenient way for technical teams so that they can work on them easily.
More important than user stories is their acceptance criteria, which you use to validate that each story is done or not. Solid tests start from writing solid acceptance criteria.
β
β
1. For each acceptance criteria, you need to write a failed end-to-end test.
2. You need to write code to make this end-to-end test pass, by writing failed integration tests that describe how your objects will interact with each other.
3. For every class, you need to write a failed unit test to test the output of each single unit.
4. Then you need to write actual code to make the unit tests pass for all classes, and the integration tests and end-to-end tests pass for all acceptance criteria.
β
β
Example TDD project
We are going to build an app that shows a list of movies (name and rating). Even a small story (feature) like this is too large to write in one go, so we need to figure out roughly the steps we might take to get there.
Here are all the features required for our sample application:
Show a list of movies. ??
Here is our user story:
As a user, I want to be able to see a list view so that I can choose my next movie.
And here is our acceptance criteria:
The app should show the name and rating of each movie.
Now we can start. ?
β
Setting up our project
First, you need to create a single view project with Unit Tests and UI Tests targets:
β
β
Most App Store apps communicate with the cloud, fetch data, and show it to the user. In order to work on a solid base on which to build features, you need to simulate the two sides that will communicate with your system. The first side is user interaction, which can be done using UI tests that trigger user events as if from an actual user. As for the cloud, you can use a framework like Swifter to simulate server requests.
β
Swifter wrapper
Swifter is a tool that will help you stub application requests and return the desired response to facilitate testing and prevent test flakiness.
β
β
We are going to make a wrapper class over Swifter to facilitate stubbing network requests.
β
β
Inside each test case, you can stub requests and return the JSON you need.
β
β
User story
As I said before, solid TDD starts from writing good user stories with clear acceptance criteria. We will represent each user story with an end-to-end UI test.
Note: You cannot write end-to-end tests for everything. If you do, you'll end up with very slow tests that you won't even be able to run every time.
To recap, here's our user story:
As a user, I want to be able to see a list view so that I can choose my next movie.
If you would like to follow along with this tutorial, I prepared a JSON file that contains a list of movies that will help us test this user story.
β
β
Writing our end-to-end test
β
β
Line 1: Stub the network request for movies and return the mock JSON file.
After writing the end-to-end test, you should make the test fail. Read the assertion messages and ask yourself if you really understand the problems from reading the messages. If not, rewrite more meaningful messages. This will help you identify problems later when you change some code and this test fails.
β
Acceptance criteria
Now itβs time to write the integration test for our acceptance criteria. To recap, here is our acceptance criteria:
The app should show the name and rating of each movie.
When writing integration tests, imagine the components of your system that will be responsible for implementing this part of the feature, and how they will communicate with each other.
Integration tests exclude the UI component and only test functionality after the UI layer (itβs supposed to be fast). If you are going to use an MVP architectural pattern in your app, your integration tests will make sure that the presentation layer will communicate properly with the model layer.
So, we will trigger an action from the presentation layer, stub the network request, and assert the callback to make sure that collaboration works between the presentation, model, and network layers.
β
Writing our integration test
With MVP, View will be responsible for displaying the data and listening to user interactions while Model will be responsible for defining the business logic and fetching, updating, and inserting the data. Presenter will make sure to decouple interaction between Model and View, and act as the communication layer between them.
β
β
Now I need to imagine how many objects I will have, the responsibility of each object, and how they will interact with each other.
β
β
Steps:
1. I need to create a mock network layer, which will return specific responses so I can assert the returned data from Presenter.
3. Presenter will use Model to fetch movies and reformat the movies to be displayed on UI components.
β
Network layer
β
β
For this tutorial, we are not going to write tests for this class, but itβs doable using a third-party library like OHTTPStubs.
β
Writing unit tests and writing code to make them pass
After finishing the design and writing the integration test, we will go through each object and write unit tests for every class, and then write code to make these tests pass.
β
MoviesListModel
β
β
β
β
β
β
β
β
We will call the network layer, parse the returned object, then return it through the delegate.
β
Congratulations, your first test passed! ?
β
MovieParser
β
MoviesListPresenter
β
β
β
β
β
Writing code to make our integration test pass
Now that we've written failed unit tests and made them pass, we can write our code to make the integration test pass.
β
β
The presenter will fetch movies from the model.
β
β
β
β
Then we can run all our unit tests and the integration test. Everything should pass. β
The final thing we need to do is make the end-to-end test pass.
β
Writing code to make our end-to-end test pass
β
β
Then, run your end-to-end test. Everything should pass. ?
β
Conclusion
If you have multiple acceptance criteria, you will need to write more integration tests for each of them to complete your user story.
In this tutorial, you learned how to implement a complete user story with solid tests without the need for backend integration. To make sure you are integrated correctly with the cloud, you will need a copy of the end-to-end test to actually run on a testing account.
Now thanks to TDD, after you develop more features in this project, if one of the above tests fail, you'll have a quick hint about which part in the code has a problem.
You can find the complete project here: Sample TDD Project.
Learn more:
- Creating UI Elements Programmatically Using PureLayout
- Catching Unsatisfiable Auto Layout Constraints in UITests on CircleCI
- How We Automate Our iOS Workflow at Instabug Using CircleCI
- How We Migrated Our Frontend While Scaling at Instabug
β
Instabug empowers mobile teams to maintain industry-leading apps with mobile-focused, user-centric stability and performance monitoring.
Visit our sandbox or book a demo to see how Instabug can help your app