How I came to refactoring tests and writing this article
As a newly arrived mobile developer at BAM, I encountered many issues with testing using Jest. To improve on this specific subject, I decided to tackle it directly and make it the number 1 focus on my mobile app project, mainly coded in React Native. I am using react-native-testing-library as my testing tool.
While I was becoming more confortable with unit and integration testing, I realized that I was always repeating the same steps, duplicating a lot of code and my test suites were becoming unreadable... it was time for some refactoring.
As I was starting this refactoring process for my tests in my mobile app project, I was looking into several articles treating this subject and realized several things :
🤯 The articles I found were very technical and difficult to understand.
🏃♂️ Refactoring tests seems like a long process. I need a way to do it faster.
👨💻 They are very few coded examples, increasing the difficulty and the abstraction. I speak only code !
I started refactoring a test suite and decided to write this article with the learning I took from it. Here is what I learned :
Why refactor tests?
Writing unit tests is an iterative process that encourages repetition and code duplication. This causes several phenomena:
When you make an error on one test, you reproduce it on all the others.
When a new developer arrives on the codebase, he/she will have difficulties reading and understanding the tests and will be induced to reproduce the same patterns.
Creating a new test in the same suite often starts with copy and paste, which prevents the developer from understanding what he/she is testing.
A tool is available for all developers who want to make their tests readable, reusable, and easily maintainable: refactoring.
This article aims to give examples of possible refactoring, at several levels of progress and difficulty.
☝ Refactoring tests can be a long and tedious process, especially when you already have a significant amount of tests, so you might as well apply those principles right from the beginning.
Scope of refactoring tests
Refactoring a test must not alter the understanding of the test itself. It is recommended to refactor only :
constants, texts, emails, codes, etc. that are found in the same test suite
repeated actions to reach the specific state of a component. Example: fill in fields with the right values and click on the submit button.
It is preferable to NOT refactor :
mocks: there are ways to mock globally for all tests, mock for an entire file or mock a specific test, which is not the subject of this article.
assertions are the core of the tests, so it is essential to leave them as is.
exceptional cases must be easily identifiable by the reviewer, in order to distinguish them from the nominal use cases.
⏰ When to start refactoring? If you find yourself :
Copying a line for the 3/4th time across multiple tests
Copying an entire test to add specific elements, change parameters
Rewriting for the 3rd time an action with different parameters
- Reading your tests and reacting like this :
...it's refactoring time !
How do I start refactoring tests?
Ok, enough rambling, let's get into the code. Based on a concrete example of a unit test on a form component, we will see 4 levels of refactoring, each of them enhancing readability and improving your development experience.
I wrote this example with React Native and React Native Testing Library. You may be more confortable with React or even another programming language, but don't worry, the steps of refactoring tests are the same.
Level 1 : Refactoring 101 🌱
Start by renaming your tests in a clear and uniform way, put hard values (string, number) in constants.
Our action event (changeText and press) are already clearer than before.
Level 2 : Refactoring the most repeated action 🌿
Refactor the most repeated action in your test, and put it in a function. This action should be used at least 3 times in your test suite.
Level 3 : declare a specific render function 🌳
At some point, you'll want to refactor several actions. Declare those in a render function specific to the test suite, making the implementation of the functions easier.
The refactored functions are now attributes of the tested component, and can be used in all your new tests.
👀 The presence of this render function allows to identify the test suites that have already been refactored, and the suites that still need some work.
Level 4 : The ultimate render function 🌏
Once you have strong refactoring foundations, you can tackle other repeated actions and develop your specific render function: the level of abstraction is up to you!
Here, I refactor the submit button as well as the action of filling all the fields and sending the form.
Result of the refactoring
Here is the result after refactoring the test :
Constants and repeated actions are reusable for other tests of the same component.
the test is easier to read, because the actions are correctly named and gathered, while assertions are still as is.
⚠️ You might notice that the final example test is much longer and more complex than the original one. However, on a project, few suites contain one or two tests. You can feel the impact of refactoring when you increase the number of tests you write. If you don't feel the need to improve readability and reduce code duplication, it may be a symptom that there are not enough tests.
To dig deeper on refactoring tests
AHA Testing by Kent C. Dodds, to better understand abstraction in testing: https://kentcdodds.com/blog/aha-testing