27 April 2021
Every software developer, at some point or another, questioned the relevance of Unit Testing and googled “Why should I write unit tests for my application?”, “What should my unit tests cover in terms of features?”, or “How can I write proper unit tests?”
Here at GO, we had the same questions when we wanted to take unit testing to a higher level and I will be taking you through our journey and how we answered these questions. Sit comfortably, take a cup of coffee or tea and let’s go!
The term “Unit Testing” is far from being a new terminology in the development industry. Records dating back to the 1950s show that the concepts behind unit testing were already being used, which is quite astounding.
Whilst I would be very excited to start this discussion from the first concepts, I want to avoid boring you with too much detail of fundamental theories and proceed directly into answering some common questions. Even if you don’t have the same queries, don’t run away; I am quite positive you will still find useful information here as well.
One last thing I want to bring in is that in this article, you will find very interesting examples of frameworks and libraries like Jest (https://jestjs.io/), Testing Library (https://testing-library.com/), and Mock Service Worker (https://mswjs.io/).
Five W & Two H
Let’s talk about a methodology called 5W2H, which is commonly used to gather basic information related to problem-solving. The 5W2H acronym stands for What, Why, When, Where, Who, How, and How much in no particular order or priority.
I will be using these questions as a guideline to help me better share our experience of writing unit tests in our React applications.
What is Unit Testing?
Unit Testing is a software testing method that focuses on (forgive my redundancy) testing individual pieces of code, called units. They are normally automated tests that run against specific snippets of the source code, making sure that a particular unit is working as it should; be it in terms of requirements or design.
Depending on the paradigm of the application, a unit can have a slightly different meaning. For example: for Object-Oriented Programming, a unit can be an entire class or a particular method, for Functional Programming, each function can be treated as units. It doesn’t matter how the code is structured and which pattern is being used, there is always a way to apply unit tests to it.
The details of a given project can influence the structure of the unit tests, as they are coupled to the application itself, the business requirements, external integrations, and things like that. Some good practices can help us create code that is easier to be tested:
A unit test commonly has one or more inputs and one or more expected outputs. A small example can be seen in the following images:
In the code snippet above we can see the implementation of two functions. Both of them are basically pure functions meaning they are free of any side effects i.e. whenever they are executed, if we have the same inputs, we will have the same outputs.
This is interesting to note as pure functions make the writing of unit tests easier than code that depends on any external values e.g. methods that execute API calls on the network or have an internal state that changes its flow.
Such impure functions require further setup like intercepting the requests and mock the expected API responses. Practically all applications have such functions and they should be tested as well, with extra attention as they are non-deterministic and therefore the results are harder to predict.
The next image shows the implementation of a couple of unit tests focused on the “calculatePriceDiscount” function. As you can see, the unit tests are very descriptive and often serve as documentation for the source code.
The implementation and runner above are using Jest as the Testing Framework.
It’s a common (and highly suggested) practice that when someone is onboarding on a new project, they get some time to go through the unit tests and see how specific pieces of code run and which business rules they enforce.
In the above example, I have intentionally left the second function uncovered and would like you to think about how this function can be tested as well. In plain English, the “roundMonetaryValue” receives a number and returns another number rounded to 2 decimal places, following the monetary rule for cents (I’m sorry currencies with different decimals, I will leave a “// TODO” to support you in the future).
We can see that transforming requirements into unit tests are, usually, not hard tasks, as they follow the usage of the functions, classes, components, and so on.
Why should we write unit tests?
The previous question already laid out a good foundation for us to answer this one. Unit tests essentially improve source code in numerous ways:
For me the key reason why I am such a big fan of unit tests is that:
I have split this last point from the rest to better explain why this is so important for everyone involved in the project.
Having a confident developer means that fewer bugs will ship to production, making the codebase easier to maintain and update. The speed of the team will increase as the unit tests will help identify possible bugs while new features are being developed. This also has a direct impact on the customer experience as it essentially removes unnecessary frustrations with bugs on production.
Of course, the unit tests themselves do not prevent all sorts of bugs from happening and thus it’s good to be used in conjunction with other testing approaches like Integration and End-to-End tests.
Given that they are cheaper to write and run faster, they make a good base for the “pyramid of testing strategies” as shown in the next image:
Whilst I have already declared my love for unit tests, I also must remain impartial here and discuss with you some downsides they can have:
If for some reason you are working on source code that is very complex to test, unit tests can be the driving force towards your team working on refactoring the existing code base to make it less decoupled, more readable, and easier to maintain.
Finally, even though unit tests do not give us the same level of assurance as end-to-end tests they are much faster to execute and less prone to break, making them a more reliable testing layer. Here I am saying that there are two sides of the same coin; we are improving execution time and reliability at the cost of assurance.
In the end, even though unit tests have their downsides and limitations, the benefits for the codebase and the culture of building high-quality software by far make them worthwhile.
When should we introduce unit tests?
There is no concrete answer to this question. However, I am going to discuss a few guidelines which might help your team take the right decision. If your team is about to start working on a new, well-defined, application the correct answer is – “from the very start”.
The best approach is to use the Test-Driven Development (aka TDD) approach i.e. start by writing the tests and then proceeding with the implementation. This mindset helps the developer think about all scenarios and permutations from the very start as opposed to the “let’s start writing code and then we see” mindset. When the developers have a better understanding of the expected logic, they can better architect the code, and therefore the code will be cleaner with proper segregation of concerns.
With TDD the developer can address functional issues from the very start as opposed to identifying them after the unit tests are developed. The tests essentially serve as a blueprint to whether you are developing things correctly or not. I am going to be honest with you; it might seem a bit tedious at first, but I can guarantee that after some time your team will be super grateful that you’ve taken this approach.
The answer is trickier if you are working on an existing code-base; say a monolith application that has grown out of proportion with tightly coupled code. In this case, writing unit tests would essentially mean re-engineering the existing code base as well. Here it makes sense to involve all stakeholders i.e. developers, POs, leaders, etc. to reach a consensus on how this technical debt can be addressed together with rolling out new features. On the next question, you will find a good direction of how this process can be tackled.
For example, at GO we choose to always use the TDD for new applications. For existing applications, we gather as a team and lay out a technical roadmap to increase the unit test coverage whilst also refactoring the codebase if required. We also choose to use TDD when developing new features or enhancements in existing applications.
Where should we apply unit tests?
The answer to this question might not be straightforward, but we found a good direction that worked for us and can help you as well.
Sometimes writing unit tests for React applications can feel a bit odd. In terms of code, we can always think of components being the unit, the smallest piece of code that we have. Components can have several different actions, dispatch events, or change internal states (unlike the pure function I’ve demoed in my previous example).
There are other concerns when dealing with user interface components, in particular, preparing a proper environment emulating the browser APIs, as they will run on Node.js. Another consideration is related to how these kinds of stateful functions should be tested, as they might not follow a standard input/output pattern.
Because of that, an easier flow to put in unit tests on a large codebase is:
Who should be responsible for writing and maintaining the unit tests?
The first thing that might come to your mind, when talking about who will be writing the unit tests, is “the developers” and that’s not wrong. Developers are the ones responsible for transforming the business requirements into code, so, they are the bridge connecting both sides. That strategical positioning makes them the best candidates to write these tests.
When the tests are fully integrated within the development processes, this becomes even more evident. As noted earlier, a TDD approach enforces that the tests are written from the beginning so they can be used as another tool for helping out the developer to deliver the feature to production.
The tests then will become part of the main source code, being developed in parallel with the user-facing features as the team keeps improving the application. It’s the developer’s responsibility to maintain the unit tests up to date and in sync with the current requirements, so they can provide as much benefit as possible.
How to write good and reliable unit tests?
With TDD writing good and reliable unit tests becomes a less prominent problem because we start by writing the unit test and adapting the code structure to fit that test rather than the other way round. If unit tests are written after, you need to ensure that you have clean units of code for which you can easily write reliable tests, indicative of what they’re testing.
This does not mean that you cannot write good and reliable tests for existing codebases but the odds are that you might need to restructure your code to make it more testable. Therefore the process of writing unit tests can serve as a driver to revisit your codebase and make it more robust, readable, and cleaner.
In a previous section, we said that writing pure functions makes writing unit tests easier. But we also said that we will face more complex functions depending on any external values e.g. functions that execute API calls on the network or have an internal state that changes its behaviour.
Using Jest as our Testing Framework already solves a significant amount of setup details we discussed previously. Since it will not run on a browser environment, Jest uses jsdom (https://github.com/jsdom/jsdom) as its underlying rendering engine, producing real HTML elements with proper attributes when we render our components.
Another important layer we have chosen to apply here at GO was based on the Testing Library. Its mindset specifies that: “The more your tests resemble the way your software is used, the more confidence they can give you.” by Kent C. Dodds (https://kentcdodds.com/).
So, our final setup will look like this:
After that, we are ready to start developing our tests in conjunction with our components:
I have a question for you: Can you already visualize the main structure of how our components will be, only by looking at the test? That’s a very interesting exercise that I want to do with you, step by step:
With that clear picture of what the functionality of the component should be, instead of how it should function, we can instantly dig into the development of such components and make the tests pass:
Take your time to analyse the source code of the components on this image (unstyled by the way, so we can focus on the functionalities), and match them with the tests we reviewed before. Also, note that if we have to introduce new features, let’s say to delete a “To Do” item, or even changing the internal structure, we wouldn’t necessarily need to update our tests and they can still be used to ensure that the usability of our application remains the same.
You can choose to test the components more individually as well, like focusing on the “TodoForm” or the “TodoList” alone. This way you can break the tree of your component into small, self-contained parts of your application to test specific behaviour.
Let’s check another example:
Again, from the tests, we can immediately identify how the component is going to be. The behaviour itself will match a given requirement and that’s where TDD shines: you first write the tests with “what has to be done” in mind instead of “how it can be done”.
There’s a new dependency that you might notice, that will take care of intercepting the calls to the network so the test can be isolated. At GO, we opted to use a package called Mock Service Worker to do this job. This is not the only solution to solve this problem, but we found out that it fits perfectly with our needs and can help to write good and reliable tests. Given that it does not require an extensive setup or boilerplate code, we can target what matters for us.
That particular component can be written as follows:
Check that the component itself does not contain anything related to the tests, nor a specific condition to change behaviour while on testing mode.
How much will unit tests impact our project?
As a team, heavily based on scrum principles, we are always trying to understand if something is or isn’t working, how we can improve processes and how our decisions are impacting the team and our customers. With Unit Testing, it’s no different. Without metrics, it’s impossible to see if unit tests are making an impact in our day-to-day tasks and if we are making our code more robust to bugs.
So, as a rule of thumb, always generate metrics for your tests, before and after applying them. This will help your team make strategic decisions. Make sure you have a good picture of the application health and how each new process is behaving positively to building a good codebase.
Furthermore, the focus should always be on the customers i.e. giving them the best experience while using your software, products, and services, as they are the main reason why your code exists in the first place.
Our journey comes to an end (for now), and I hope I’ve provided you with some good insight regarding the importance of applying Unit Testing on your projects, making them more reliable and less error-prone.
About the Author and the Team
Marcelo Dasilva is one of our Frontend Developers, with 7 years of experience in creating high quality, user-centric web applications. Being part of the team led by Hayley Bugeja, our Online Development Team Leader, within the Digital Team of GO, they are responsible for delivering first-class user experiences to our main website and customer portal.
Have a look at our careers page and get in touch with us if you want to take your career to the next step! For GO products and services, visit our official website or get in touch with our experts online or by visiting any of our GO outlets.