Unit Testing: Trials and Tribulations of a Newbie (part 1)

Jihan
The AB Tasty Tech & Product Blog
7 min readJan 5, 2021

“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.” -Martin Golding

A few months ago, I joined AB Tasty as a Front End Developer, and with my feature team, we have been assigned a specific project: migrating all the leftover components of our main application from AngularJS to React. Indeed, few years ago, we changed our framework to a more modern and appropriate one.

This project was a great opportunity for me to improve my code quality by focusing more on testing. So as part of the migration process and in order to highlight the huge importance of testing, we established a new rule: Unit test each migrated component.

The problem was, I had little experience with testing and I had no clue where to start!

In this article, I’ll share with you how I led this project and the best practices I learnt from it, from the tools we chose to their implementation.

But before we get to the details, let’s explore the meaning and the importance of Unit testing.

What is Unit Testing and why is it important?

Unit testing is a huge part of the development of our application. It enables us to check the behaviour of every piece of code that we write, even the smallest parts. Hence its name “unit test”.

Unit testing is one of the topics that used to scare me. Before joining AB Tasty, in previous companies I worked for, I had always been told to deliver code without “wasting” my time on testing it or that it was the “QA’s job to do it”…

That’s why I had limited experience with unit testing. Instinctively, I knew that it was nonsense, because, as developers, we are expected to deliver a fully functional, bug-free application. Unit tests aim to check the code at the first level, ensuring its robustness for future use. In case of a bug, they will help catch it at source.

And this is why the test pyramid approach is so valuable:

On the pyramid of testing, most of the testing should take place at the development stage. These tests are the easiest, cheapest and quickest. They are the base layer that enable us to check our work as we go, and prevent bugs before UI testing.

In our unit test implementation journey, the first step was to select the right tools for the job.

Here’s what we decided:

  • Using JEST as a test runner.
  • Using React Testing Library (RTL) as a testing utility tool.

Choosing our test runner: Jest

Jest is one of the more popular test runners and the default choice for React projects.

As a newbie, the first things you need to know are: what is Jest and how to use it?

What is Jest?

Jest is a testing framework, created and maintained by Facebook, which has great integration with React. It has an extensive documentation library, which makes its setup and configuration smooth and simple.

Jest provides a few key features such as:

  • Managing parallelisation and asynchronous methods
  • Executing several assertion methods
  • Mocking functions
  • Providing a code coverage report

Basic testing syntax to use Jest

We will talk in more depth about this rendering syntax (I’ll talk about the render() method) in my next article. But first let’s see the basic methods that we can use in any test file.

describe() is a Jest method containing one or more related tests. It takes into account the two following parameters:

  • a string describing the test suite.
  • a callback function wrapping the actual test.

Usually when we need to describe a specific behaviour of our component, we wrap our suite of tests in a describe block.

it() allows us to describe what a test should achieve. It takes two parameters, a string describing what the test is going to do, and a callback function where we write our test. Then, we use expect and matchers within this function.

expect() describes what we are expecting from the code we have written. The syntax is the following: expect().matcher()

matchers are a collection of methods in Jest. They enables us to test our values in multiple ways.

Here are some commonly used matchers :

  • toBe uses strict equality
  • toEqual check the value of an object

Sometimes, before running our tests we want to do some initialisation. For that we could use beforeAll and beforeEach in a describe block :

  • If we are sure that the tests don’t make any changes to the setup conditions, we can use beforeAll as it is only called once before all the tests.
  • If the tests make changes to those conditions, then we would use beforeEach as it runs before each test, so it can reset the conditions for the next one.
  • Using both, beforeAll and beforeEach. Usually, the slow network call is put in beforeAll, so it only has to happen once. The data object which presumably is modified by the tests is reset each time in beforeEach.

Just as we can do with the setup, we can also perform something after each test runs using afterEach, and after all tests end using afterAll in a describe block as well.

Opting for a testing utility tool: Enzyme vs. react-testing-library

We used to use Enzyme, another react testing library created by Airbnb. It allowed us to render our React components and to check their implementation, but we soon realised that instead of testing our code implementation, what we really wanted to test was the interaction with our users.

The core of AB Tasty as a company is customer experience optimisation, helping brands to improve user experience. Our users don’t care about how our platform is implemented or what technology we are using, they want to use our tool for its functionalities.

That’s when we learned about react-testing-library: a simpler replacement for Enzyme, which also encourages good testing practices.

What is react-testing-library?

The react-testing library is an open-source testing library created by Kent C. Dodds and maintained by a large community of developers. Basically, it’s a light-weight solution for testing React components without touching their implementation details. Its guiding principle is the following:

The more your tests resemble the way your software is used, the more confidence they can give you.

React-testing-library was the most important discovery during this whole process. It seemed more appropriate for our unit testing, considering our mindset and the library’s.

So, does this replace Jest? Absolutely NOT, it works WITH Jest as Jest is a test runner, but it does replace Enzyme as a testing utility for React.

Why did we opt for react-testing library ?

The react-testing-library structured our tests in a way that doesn’t include any implementation details, which in turn allowed us to manage more powerful test cases, and keep the user’s perspective in mind. This specificity makes it easy to adapt our projects in the future. So when we need to change our implementation or when our components are refactored, our tests won’t break.

React-testing-library enables us to work with the DOM nodes specifically, where Enzyme was about working with react components and checking props and state…

For the most part, this library is about what is output on the DOM at this time. We can use individual DOM nodes and interact with them, which makes testing much easier, and saved us a lot of pain and time!

On top of this, react-testing-library forced us to think more about our best practices, because it made us use selectors in order to query and get access to some elements.

Let me share with you how to use react-testing-library.

How to use react-testing-library?

As our tests should resemble the way the user interacts with our application, we should avoid implementation details. For that, react-testing-library has different methods of querying DOM elements for tests.

There are six variants of query methods that we use depending on what we are testing:

  • getBy*() returns the first matching element and throws an error if no element is found or if more than one element is found.
  • queryBy*() returns the first matching element, and returns null if no element is found.
  • findBy*() returns a promise that resolves with a matching element, or rejects if no element is found or if more than one element is found after a default timeout of 1000 ms.
  • getAllBy*(), queryAllyBy*(), findAllBy*() works as mentioned above, but instead, it returns all matching elements and not only the first one.

Usually, when we want to test for the presence of an element, and we are not sure that the element exists , we use queryBy*() as it returns null instead of getBy*() that throws an error.

And the queries available for those variants with the recommended order of priority are the following:

  1. getByLabelText() finds the label matching the given TextMatch* .
  2. getByPlaceholderText() searches for an element by its placeholder text and finds one that matches the given TextMatch.
  3. getByRole() finds an element by its ARIA role.
  4. getByText() searches for an element by its text and finds one that matches the given TextMatch.
  5. getByDisplayValue() returns the input, textarea, or select element by its value.
  6. getByAltText() finds an image by its alt text.
  7. getByTitle() finds an element by its title attribute.
  8. getByTestId() finds an element by its data-testid attribute.

TextMatch* can be a string, regex or function that returns true for a match and false for a mismatch.

Conclusion

We now have the basics to start writing our first test. In my next article, I’ll share more about how we implemented our test and talk in-depth about react-testing-library helpers, how to test user actions, managing asynchronous tests and so on…

--

--