Jan 07, 2023

Write Unit Tests with Jest in Node.js

Write Unit Tests with Jest in Node.js

Jest is a clean and concise JavaScript testing framework developed and maintained by Facebook.

$ npm install --save-dev jest

Running test files

With Jest, a test file is any file within a project that has the following extension:

*.test.js

To run all the test files at once, we can add the following script to the package.json file:

{
  "scripts": {
    "test": "jest"
  }
}

And execute the following command:

$ npm run test

Running a single file

Alternatively, to run a single test file, we can use the following syntax, where in UNIX, the double dash -- is used to indicate that following arguments should be treated as positional arguments and not options.

$ npm run test -- file.test.js

Running test suites

Jest also offers the possibility to run tests in a more targeted fashion.

Since a project often includes several kinds of tests, such as unit tests or integration tests, we can prefix each test file with a keyword that describes its type:

*.unit.test.js
*.int.test.js

And update the package.json file the following way:

{
  "scripts": {
    "test-unit": "jest unit",
    "test-int": "jest int"
  }
}

We can then execute either one of these commands to specifically run the desired test suite:

$ npm run test-unit

Writing tests

To write a test, we can use the test() function that takes as arguments:

  • A string of characters describing the test.
  • A callback function containing one or more logical assertions.
test('test description', () => {
  // logical assertion
});

A logical assertion is a statement that asserts that a certain premise is true. In Jest, these statements are created using the expect() function that takes as argument the value we want to test, and that can be chained with a set of "matchers" that allow us to validate different things.

test('test description', () => {
  // Compares primitive values or checks referential identity of object instances.
  expect(result).toBe(value);

  // Compare recursively all properties of object instances.
  expect(result).toEqual(value);

  // Ensures a value is false in a boolean context.
  expect(result).toBeFalsy(value);

  // Compares number or big integer values.
  expect(result).toBeGreaterThan(value);
});

The list of matchers is available here: https://jestjs.io/docs/expect.

It is also possible to regroup several tests belonging to the same scenario by wrapping them in the describe() function, that takes as arguments:

  • A string of characters describing the scenario.
  • A callback function containing the test functions.
describe('scenario description', () => {
  test('test_1 description', () => {
    expect(result).toEqual(value);
  });

  test('test_2 description', () => {
    expect(result).toBeGreaterThan(value);
  });
});

Testing synchronous code

Let’s consider the following sum() function, that either returns the sum of two integers, or throws a TypeError if any of the parameters is not a number.

function sum(a, b) {
  if (isNaN(a) || isNaN(b)) {
    throw TypeError();
  }
  return a + b;
}

module.exports = sum;

To verify that the sum() function returns the sum of two numbers, we can write the following test:

const sum = require('./sum');

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

To verify that the sum() function throws an error when either one of the operand is not a number, we can write the following test:

const sum = require('./sum');

test('it should throw an error when an operand is not a number', () => {
  expect(() => sum(1, 'a')).toThrow(TypeError);
});

Note that in order to test that a function throws, we must wrap it in another function, as otherwise the error will not be caught, and the assertion will fail.

Testing asynchronous code

Let’s consider the following fetch() function that takes as argument a number, and either returns a resolved Promise if the number is even, or an unresolved Promise otherwise.

function fetch(n) {
  if (n % 2 === 0) {
    return Promise.resolve("EVEN");
  }
  return Promise.reject("ODD");
}

module.exports = fetch;

To test for a resolved Promise, we can define an asynchronous callback function using the async keyword, and wait for its fulfilment using the await keyword:

const fetch = require('./fetch');

test('it should resolve the string "EVEN" when the argument is 2', async () => {
  const data = await fetch(2);

  expect(data).toBe('EVEN');
});

On the other hand, when testing for an unresolved Promise, we must wrap the call of the function we’re testing in a try..catch block:

const fetch = require('./fetch');

test('it should reject the string "BROKEN"when the argument is 2', async () => {
  try {
    await fetch(3);
  } catch(error) {
    expect(error).toMatch('ODD');
  }
});

Related posts