I got stuck when writing unit tests
When I started writing unit tests, I faced a problem that you are most likely facing now. I've watched some unit test tutorials, but I got stuck and didn't know what to test when I put it into practice.
Should I test everything? Only the domain components? Should I test the CocoaPods I've been using? Should I test every single branch statement in my code? That feeling is terrible and almost made me give up.
After answering these questions, I caught myself in another tricky situation. My tests became too hard to maintain. But what was I doing wrong?
Key tips to know what to test
I didn't know what to test, and to solve that, I followed some simple tips. Check it out!
1. Follow design principles
As hard your test cases are, as badly designed your code is. This means that if your test is becoming hard to maintain, you have code smells in the subject under the test.
See some red flags warning you that your code should be redesigned.
- Massive test setup and teardown
- Big test methods
- Big test case classes
- Flaky tests
In this case, we need to check our code and refactor it due to applying some design principles. I recommend you study more about Design Patterns, Software Design Principles, and SOLID. However, to save your time, I strongly recommend you to explore the Dependency Injection and after Dependency Inversion Principle.
Dependency Inversion is the key to testability. When you apply it in your project, your code becomes more testable and enables us to use Test Doubles to pretend some system behaviors to achieve the real pure unit test.
The other design patterns also will be conducive, but you had better master the Dependency Injection and Inversion as soon as possible.
2. Test over the public API
A good practice that we follow is to control the class access through the
public modifiers. And this makes our test hard at a glance.
We should test our classes from the point of view of its consumers. Therefore, we need to test over the class’ public API.
We don't need to let all the class property and method public in favor of testability. This will bring us some design problems and some unexpected behavior. After all, we need to protect ourselves from ourselves. So, focus on testing the public API, which the class consumers actually use.
But what about my private API?
We will eventually test them implicitly by testing the public API. We need to configure the appropriate inputs to execute the private methods. When some private API becomes too hard to execute, it may indicate that your class has too much responsibility. It's time to refactor.
3. Test the behavior, not the structure
As beginners, we tend to look for each
guard, and design all tests to cover these branches. Well, that's a good strategy, but it's not my favorite.
Testing by looking at code structure leaves our test cases referring to code structure. As a result, always when our code suffers refactoring, the test will also have to be refactored. Our tests become rigid and hard to maintain.
I prefer to test the app's behavior, the use-cases, the features. In this case, the tests will only need to be changed if the behavior of the app changes. Otherwise, we can freely refactor our code, and the tests will continue to work. That's one of the main benefits of the unit tests: Make the software soft and easy to change.
Testing the behavior also implies that we won't need a test class for every single production class in our app, because we can implicitly test, through integration, more than one class.
4. Don't test third party code
Finally, I would say that we don't need to test third-party code. I always assume it is already tested. For example, I don't need to write tests to make sure Decodable works because I assume Apple has already done that. Same idea for an open-source code like Alamofire. It's not a rule if you feel safer writing tests for third-party code, no problem! In general, we can assume that third parties have already tested these codes.
If after all these tips you are still feeling stuck, don't worry, step back and see this advices:
Test your code first
Testing code you haven't written can be tricky. It will become progressively easier:
- I strongly suggest that you start by testing new classes that you are creating. It is much easier to add tests to something that is being created from scratch.
- The next step is to add tests to the code you wrote a while ago.
- Then write tests for bug fixes.
- Here you start to have contact with code that is not yours.
- Finally, you will be adding tests to the code you are working on, whether you wrote it or not.
I did it this way and I highly recommend it. It is much easier to first learn how to test in our code. Once we've learned, it's just a matter of moving on to other people's codes.
Lack of Requirement Analysis
Sometimes we got stuck because we don't fully understand the feature we are implementing. Step back and write down all the requirements for that feature. Do it with the Product Manager, Team Manager, Designer, and your pairs. It's part of the project planning and I don't recommend writing any line of code before you fully understand the feature.
Collect the happy path and all the possible error courses that the feature can run. After all, start implementing the feature.
Mastering unit testing demands practice and consistency. Don't be overly depressed about it, even if it is hard for you. Just keep trying, slowly, step-by-step, every day, and unit testing will become as natural as breathing for you.
To wrap it up, it's normal to get stuck when writing unit tests. It doesn't mean we don't know how to write unit tests but indicates that we need more attention on what to test.
This is my second article and I hope it helps you. Next posts I will start writing some Swift testing tips. Stay tuned! See ya!