Testing Node.js Integrations with Jest

Published

I recently used Jest to write tests for a few Node.js Lambdas. It was my first time using the testing framework, and while there was a learning curve, I was ultimately impressed.

Jest has extensive mocking capabilities, which helped me test code with integrations (e.g. external REST API calls). It also had an intuitive matching API, which made for easy validation when writing tests.

Below are a few things I learned along the way, starting with some general pros/cons of using Jest.

Pros

  • Full-featured testing framework supporting the entire JavaScript spectrum (Node.js, Typescript, React, etc.)
  • Extensive mocking functionality from single objects and functions to entire ES6 modules (and the ability to inject mocked modules during normal import statements)
  • Intuitive matching API (expect()) supporting common JavaScript data types (e.g. arrays) and deep object comparisons

Cons

  • Mocking can become very complex and include a learning curve (especially when mocking ES6 modules)
  • Jest documentation is difficult to understand at first
  • Stack Overflow and community posts seem centered around edge-cases, not beginner questions

Sample Code Posted to GitHub

For demonstration purposes (and my own sanity), I posted a sample project to GitHub that includes a lot of the capabilities and features described in the rest of this post. Check it out as a good place to start!

Basic Jest Examples

Jest can be installed just like other NPM/Yarn modules (npm install jest). The GitHub page has a helpful section covering the install, and what a basic test might be like.

import request from 'supertest';
import app from '../../app.js';

test('/hikes should return a list of hikes', async () => {
  const response = await request(app).get("/hikes");
  expect(response.statusCode).toBe(200);
  expect(response.body.hikes.length).toBe(3);
});

Here is a sample Jest test for a Node.js Express REST API. The supertest module is used to mock the Express request to /hikes, and the expect() matchers are used to confirm aspects of the response.

Mocking Integrations

Jest includes the ability to mock JavaScript Objects and Functions. When abstracting integrations like REST API interactions into a separate class/modules (like what is done with the hikeService.js in the example code), Jest can be used to mock these complete integrations.

For example, the hikeService.js makes calls out to an external GraphQL API. By grouping all of these calls into a single module, we can use Jest to mock the entire module, and simulate responses that are confirmed in the tests.

Use of ES6 Modules Required

One thing to note about the techniques outlined here is that when creating Manual Mocks in Jest, the module/code being mocked must be imported as an ES6 Module (as opposed to using the CommonJS require() syntax). This link provides an explanation of differences between the two module import formats.

Switching to the ES6 module import syntax (import statements) may introduce the requirement for Babel transpilation, which is configured in a babel.config.json file (here is an example). Jest uses Babel behind the scenes to “hoist” import statements so that the mocks are included instead the modules they are replacing 1.

Global Mocks

Jest Global Mocks provide a structured and seamless way of mocking of JavaScript modules and services. They can be used to quickly mock entire modules, and inject the mocks where ever they are used in the code base. To use Global Mocks:

  1. Create the Global Mock file adjacent to the module in the code base – here is an example with custom responses, which is placed in a __mocks__ folder adjacent to the service it is mocking.
    1. Any methods that are not explicitly mocked will be defined by an “automatic mock”.
  2. Include the Global Mock in any tests that will use them with a jest.mock() call – here is an example.

Mocking Values within Individual Tests

Jest provides the jest.spyOn() functionality which enables mocking specific methods of a service/module, rather than the entire thing. This can also be done on a per-test basis if different responses/outcomes are desired. Below is a code snippet taken from the sample tests:

test('confirm hike time ranges used during getPhotosDuringHike()', async () => {

  jest.resetAllMocks();
    
  // mock getHike() method call with response specific to this test
  //  note: requies hikeService to be a valid ES6 module for this spy to be hoisted
  jest.spyOn(hikeService, 'getHike').mockImplementation(() => {
    return {
      "hikes": [
        {
          "route_id": 123,
          "title": "My Mocked Hike",
          "stats": {
            "startTime": "2019-10-30T16:49:40Z",
            "endTime": "2019-10-30T19:05:28Z"
          }
        }
      ]
    }
  });

  // spy on hikeService.getPhotosByTimeRange() with a mock implementation provided and 
  //  track spy reference in getPhotosByTimeRangeMock for assertions later in test
  const getPhotosByTimeRangeMock = jest.spyOn(hikeService, 'getPhotosByTimeRange').mockImplementation(async () => {
    return JSON.parse(fs.readFileSync('./test/resources/json/mock-get-hike-photos.json'));
  });
  
  await request(app).get("/hikes/123/photos");
  
  // confirm mock was called once - make sure to reset mocks inbetween tests otherwise
  //  .calls will include calls from all tests instead of just this one
  expect(getPhotosByTimeRangeMock.mock.calls.length).toBe(1);

  // confirm parameters passed to mocked function
  expect(getPhotosByTimeRangeMock.mock.calls[0][0]).toBe('2019-10-30T16:49:40Z');
  expect(getPhotosByTimeRangeMock.mock.calls[0][1]).toBe('2019-10-30T19:05:28Z');

});

In the example above, we are using jest.spyOn() to accomplish two things:

  1. Mocking the hikeService.getHike() method to return a custom object specific to this test (lines 7-20).
  2. Mocking the hikeService.getPhotosByTimeRange() method to return JSON defined in a local file, and saving the reference to the mocked function into the getPhotosByTimeRangeMock variable (line 24).

The mock reference saved in number 2 above contains a .mock property, which is added to all mocked functions in Jest. The .mock property is then used in the test to confirm the parameters passed to the mocked hikeService.getPhotosByTimeRange() function (lines 35-36).

The Jest documentation describes more ways to make use of this powerful .mock handle.

Other Mocking Strategies

Jest is a powerful framework that can be used in many other ways in addition to what is mentioned above. Additional mocking strategies are out there, and may be more helpful given the situation.

Debugging Tests with VS Code

While writing these sample tests, I found it very helpful to install the Jest Runner VS Code extension, which made for easy execution and debugging of tests in VS Code.

References

  1. Stack Overflow – Jest testing context / spy on mocked variables created outside of functions (class level) Postmark

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *