⏩ Writing cleaner Jest tests 🚀

Wojciech Mikusek Tech Lead

Unit and integration tests are an essential part of a daily software developer job. Sometimes it may take more time to write a proper test suite than to actually implement the functionality. That’s why writing well-organised and readable tests is one of the most important skills every developer should acquire. In this article, I will provide you with some tips that will help you with writing clearer tests.

Jest is a dominant JavaScript testing framework with over 22 millions weekly downloads from NPM in late 2023. It provides a wide variety of tools for writing good test cases, however, it is not focused on the structure of larger test suits. There are some alternative libraries but they are far less popular as shown in the picture below.

Source: https://npmtrends.com/jasmine-vs-jest-vs-mocha-vs-vitest

I have a Ruby on Rails background where RSpec –   is a go-to test framework. It describes itself as “Behaviour Driven Development for Ruby” which is “making TDD productive and fun.” – source: https://rspec.info/. Let’s see how can we introduce some of this BDD and other good practices into Jest written tests.

 

1. Use many nested describe blocks, instead of specifying context within test message


In many projects that are using Jest, test suits tend to look like this:

As an example we could take a look at TensorFlow.

With simple unit tests this approach could be fine, but as the number of variants grows, the test suite starts to become less readable. During code review or when implementing new requirements, the developer may struggle to check if all the edge cases are thoroughly covered.  

A simple solution for this is to move context outside of the test message. This approach originates from Behavior-Driven Development (BDD) development process which organises tests in specifications based on behaviour rather than on simple inputs. It is utilised by many test frameworks like RSpec.

RSpec provides additional key word “context” which is used instead of describe to wrap tests against one functionality under the same state. In Jest we can just stay with using only describe, but optionally we could add a new global using Jest Plugin Context

 

Assume we have a method that checks if the user’s age is above 18 (isAdult). Typical Jest test suit would look like this:

Ok, but now what will happen if the age of majority is dependent on the place you are? We should add new test case and adjust test messages

 

Now our test messages start to become quite long and not so easy to read. In the business reality we may have several cases to handle in such a method like expiry, uniqueness checks, some other business validation. Such test suite structure is not maintainable in the long run or will result in messages that do not explain what really is happening in the test. Luckily we can try to refactor this:

Ok, but now what will happen if the age of majority is dependent on the place you are? We should add new test case and adjust test messages

 

2. Use beforeEach block to setup prerequisites from describe block

 

In the classic Jest test suite structure beforeEach blocks are used to setup prequalities to run tests like setup of the database. However, they are not used as much as they could be. Moreover, the developer can leverage BDD structure described above to combine describe and beforeEach blocks. Let’s imagine scenario: 

 

  • The user wants to post a job offer  

  • The job offer has some validations eg. needs to provide salary range, proper description

  • Only specific type of users can post a role – validation is handled by a service which is mocked in the test

 

How this test would look like in classic Jest structure:

As we can see, this code has a lot of duplications and with more attributes to be validated during a job offer creation, it  would become difficult to read. Let’s refactor this using patterns described above:

As we can see, beforeEach blocks and constants are linked with what is described in the describe block. It increases tests readability and would be easier to maintain.

 
 

 3. Use default params and override only changing attributesUse default params and override only changing attributes


In the test example above we still have one problem – for each test we define new jobOffer object. With just 2 attributes it is not a problem, but we can easily imagine much more fields that would result in a interface like this:

With such a structure copying the object and changing 1 attribute for each test would be highly unreadable as it is difficult to actually spot which attribute is different and tests get longer. Solution for this is to have a default constant fulfilling the interface and just override the attribute the test is referring to. Let’s see the example:

4. Use factories for creating objects or records in DB


One of the things I noticed when I switched from Ruby on Rails (RoR) to NodeJS is a different way how DataBase records are populated in tests. In RoR the default way is to use factories with gem FactoryBot, while NodeJS favours manual SQL inserts.

Using factories offers many benefits over manual SQL inserts

  • Requires less code and is more DRY

  • Developer doesn’t need to care about all required fields in the SQL table (e.g. created_at will be filled automatically), only those that are tested

  • No need to rewrite tests after adding new fields

  • Offers high readability with traits and default values

  • Types checks (when using typescript)

  • Automatic creation of related records in other tables

Luckily, ThoughtBot offers implementation of FactoryBot in JavaScript – Fishery. In my current project we use it with great success saving thousands of lines of code. Using factories provides a similar effect to using default params, in tests you only specify attributes you care about in a given scenario.

Conclusions


As I stated above, the most important part of writing good tests (apart from actually testing the code) is readability. Other developers in your team need to understand what scenarios are being covered in your tests and what may be missing. It could help them to easily add new tests when requirements change or to spot any missing edge cases. In the article I’ve shown some tricks that we use in Exlabs to write cleaner and more maintainable tests using the Jest testing framework.