Writing Unit Tests in Jest

12 min read·Jan 1, 2025

Unit testing is a software testing technique that consists in testing the smallest individual components of a software application (i.e. a "unit") in complete isolation from external dependencies such as a database, the filesystem, or HTTP services.

Its goal is to test the logic of these units rather than their interaction with other components, in order to ensure that they behave as expected.

In general, unit tests should be:

  1. Automated, making them easy to run after each codebase update to ensure that nothing was broken in the process.
  2. Fast and kept as short as possible, since otherwise, you and other developers working on the project will be less inclined to run them.
  3. Readable, as they act as the best form of documentation since they are not supposed to get out of sync with the code they document.
  4. Deterministic, which means that no matter how many times we run them, they should always behave the same way as long as the underlying code has not changed, as otherwise, there is no reason for developers to trust them.

It is important to note that while unit testing is designed to improve the code quality by facilitating the detection of bugs early on in the development cycle and the refactoring of existing code, unit tests alone are not enough to prevent integration or system-level issues, and must be complemented by other types of tests, such as integration or end-to-end tests.

Unit tests and test cases

A unit test is often composed of several test cases (scenarios) to which the tested unit must respond in a consistent and predictable manner.

Each test case has its own set of inputs and conditions, and is meant to cover a specific aspect of the unit's behavior using one or more assertions, which are statements that verify whether a condition is true.

Common assertions often include testing whether the unit outputs or returns a predefined value, calls a specific function during its execution, throws a specific type of error if its inputs are invalid or missing, and so on.

Declaring a test case

In Jest, test cases are declared using the global test() or it() function as follows:

test(description, callback);

Where:

  • description is a short string of characters describing the test case.
  • callback is a function containing one or more logical assertions.

Note: In Jest, the test() and it() functions are global and do not require to be explicitly imported into the test file.

Declaring an assertion

Within the callback function of the test() or it() function, assertions are created using a combination of the expect() function and a "matcher" function as follows:

test(description, () => {
  expect(expression).matcher(expectedValue);
});

Where:

  • description is a short string describing the test.
  • expression is the value of the expression you want to test, usually the return value of the unit.
  • matcher is a chainable function that takes as argument an optional value that will be compared to the resulting expression.
  • expectedValue is the expected value the expression will be compared against.

Notes:

  • In Jest, the expect() function is global and doesn't require to be explicitly imported into the test file.

  • You can use the not modifier to negate an assertion:

    test(description, () => {
      expect(expression).not.matcher(expectedValue);
    });
    

Example

In this example, the calculator module exports a single function named add that returns the sum of two variables:

// File: calculator.js

module.exports = {
  add: (a, b) => {
    return a + b;
  }
};

To assert that the add function does return the sum of two numbers, we can import it into a test file and write the following test:

// File: calculator.test.js

const { add } = require('./calculator');

test('it should return 3 when operands are 1 and 2', () => {
  expect(add(1, 2)).toBe(3);
});

Where the toBe() matcher function checks whether the return value of the add function equals 3.

Good practices

Here are a list of good practices you should follow to improve the clarity, readability, and maintainability of unit tests.

Follow the same naming convention

Each test description should follow the same naming convention using the formula: "should + expected behaviour + when + state under test".

For example:

it('should return 3 when operands are 1 and 2', () => { ... });
it('should throw an error when operands are not numbers', () => { ... });

Write BDD-style test cases

Each test should follow the Arrange, Act, Assert pattern, or in Behavior-driven development the Given, When, Then pattern, where:

  1. Arrange / Given: sets the initial context and test data.
  2. Act / When: executes the unit being tested.
  3. Assert / Then: verifies the expected outcome.

For example:

it('should return 3 when operands are 1 and 2', () => {
  // Given
  const expected = 3;

  // When
  const result = add(1, 2);

  // Then
  expect(result).toBe(expected);
});

Only use one assertion per test

Each test should focus on a single aspect of the unit's behavior and outcome, rather than its internal implementation details, and should preferably have only one logical assertion.

Cover all edge cases

Units should be tested against both typical and edge cases to ensure that they properly handle all possible scenarios, including errors and unusual inputs.

Avoid using magic values

In programming, magic values (or magic numbers) refer to unique values with unexplained meaning or multiple occurrences in code.

Rather than used directly, magic values should be assigned to constants with explicit names that describe specifically why these values were chosen.

For example:

it('should return a decoded password when the hash and the secret are valid', () => {
  // Given
  const passwordHash = 'kmdS@#Ai39mA-mxI#_';
  const encryptionSecret = '39kdsxD';
  const expectedValue = 'hello_world';
  
  // When
  const decodedPassword = decodePasswordHash(passwordHash, encryptionSecret);

  // Then
  expect(decodedPassword).toBe(expectedValue);
});

Avoid relying on external dependencies

Since unit tests are supposed to be deterministic, they should never depend on:

  • Other test cases.
  • Environmental values that are likely to change, such as the current time.
  • The file system, the network, or APIs that are likely to fail.

Summary

Here's a summary of what you've learned in this lesson:

  • Unit testing is a testing technique that consists in testing the smallest individual components in complete isolation.
  • Unit tests should be automated, fast, short, readable, and deterministic.
  • Unit tests are made of test cases (i.e. scenarios) used to cover every aspect of a component's behavior.
  • An assertion is a statement that verifies whether a condition is true.
  • Test cases are declared using the test() or it() function.
  • Assertions are declared using the expected() function and a "matcher" method.
  • Test case descriptions should follow the formula: "should + expected behaviour + when + state under test".
  • Test cases should follow the structure: "given / when / then".
  • Test cases should ideally contain only one assertion.
  • Test cases should not contain magic values.
  • Test cases should not rely on external dependencies.

Enjoying the courses?

I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:

Support BackendBrewery
Writing Unit Tests in Jest | Backend Brewery