loading...
SinnerSchrader Engineers

Testing with Jest and TypeScript, the tricky parts

terabaud profile image Lea Rosema ・Updated on ・3 min read

Recently, I started with a side project that uses TypeScript in the frontend and in the backend. I wanted to do things test-driven and chose the Jest framework as it is a very popular choice.

When using Jest with TypeScript, I encountered some struggles and pitfalls I ran into.

I would like to share the learnings I had πŸ‘©β€πŸ’»πŸ™‡β€β™€οΈπŸ€·β€β™€οΈπŸ€¦β€β™€οΈπŸ‘©β€πŸŽ€πŸ˜Š.

Using Jest with TypeScript

In the first place, jest recommends to use TypeScript via Babel in their documentation.

I couldn't get Babel configured correctly, so it did not work for me. I used the alternative approach via ts-jest:

npm install --save-dev jest typescript ts-jest @types/jest
npx ts-jest config:init
Enter fullscreen mode Exit fullscreen mode

It generates a jest.config.js file with:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};
Enter fullscreen mode Exit fullscreen mode

If you are testing browser stuff, you can change testEnvironment to 'jsdom' to get access to DOM APIs in your tests.

Mocking stuff in TypeScript

When I first tried to use mocks in TypeScript, I got a lot of type errors when trying to access properties from the mock (eg. mockClear()).

I figured out ts-jest provides a mocked() wrapper function that adds all mock properties to the function or object you would like to mock.

Example:

Let's look at an example app that fetches a person from the Star Wars API

// api.ts

import fetch from 'node-fetch';

const BASE_URL = 'http://swapi.co/api/'

export async function getPerson(id: number) {
  const response = await fetch(BASE_URL + `people/${id}/`);
  const data = await response.json();
  return data;
}

// index.ts

import { getPerson } from "./api";

async function main() {
  const luke = await getPerson(1);
  console.log(luke);
}

main();
Enter fullscreen mode Exit fullscreen mode

The testing code mocks fetch and provides a mock implementation for it:

// api.test.ts

import fetch from 'node-fetch';
import { mocked } from 'ts-jest/utils';
import { getPeople } from './api';

jest.mock('node-fetch', () => {
  return jest.fn();
});

beforeEach(() => {
  mocked(fetch).mockClear();
});

describe('getPeople test', () => {
  test('getPeople should fetch a person', async () => {

    // provide a mock implementation for the mocked fetch:
    mocked(fetch).mockImplementation((): Promise<any> => {
      return Promise.resolve({
        json() {
          return Promise.resolve({name: 'Luke Vader'});
        }
      });
    });

    // getPeople uses the mock implementation for fetch:
    const person = await getPeople(1);
    expect(mocked(fetch).mock.calls.length).toBe(1);
    expect(person).toBeDefined();
    expect(person.name).toBe('Luke Vader');
  });
});
Enter fullscreen mode Exit fullscreen mode

Ignore css/scss/less imports

By default, jest tries to parse css imports as JavaScript. In order to ignore all things css, some extra configuration is needed.

Add a stub-transformer to your project which returns css imports as empty objects:

module.exports = {
  process() {
    return 'module.exports = {};';
  },
  getCacheKey() {
    return 'stub-transformer';
  },
};
Enter fullscreen mode Exit fullscreen mode

Add this to your jest.config.js configuration file:

module.exports = {

  // ...
  transform: {
    "\\.(css|less|scss)$": "./jest/stub-transformer.js"
  }
};
Enter fullscreen mode Exit fullscreen mode

node Express: mocking an express Response object

I was struggling with testing express routes on the backend side. I figured out that I don't need a complete implementation of the express response object. It's sufficient to implement just the properties you actually use. So, I came up with a minimal fake Partial<Response> object, wrapped into a Fakexpress class:

import { Response } from 'express';

export class Fakexpress {

  constructor(req: any) {
    this.req = req;
  }

  res : Partial<Response> = {
    statusCode: 200,
    status: jest.fn().mockImplementation((code) => {
      this.res.statusCode = code;
      return this.res;
    }),
    json: jest.fn().mockImplementation((param) => {
      this.responseData = param;
      return this.res;
    }),
    cookie: jest.fn(),
    clearCookie: jest.fn()
  }

  req: any;
  responseData: any;
}
Enter fullscreen mode Exit fullscreen mode

The test code looks like this:

  test('Testing an express route', async () => {
    const xp = new Fakexpress({
      params: {
        name: 'max'
      }
    });

    const searchResult = {
      name: 'max',
      fullName: 'Max Muster',
      description: 'Full Stack TypeScript developer',
      pronouns: 'they/them',
    };
    await expressRoute(xp.req as Request, xp.res as Response);
    expect(xp.responseData).toStrictEqual(searchResult);
    expect(xp.res.statusCode).toBe(200);
  });
Enter fullscreen mode Exit fullscreen mode

Discussion

pic
Editor guide
Collapse
wolfhoundjesse profile image
Jesse M. Holmes

I ran into something in this category with mocking a module with jest.mock().

import { fetchResource } from 'utils/api'

jest.mock('utils/api')

This alone is enough to mock fetchResource, but with TypeScript we still get the intellisense from the original function rather than the mock function. I just reassigned fetchResource and typed the new function:

import { fetchResource } from 'utils/api'

jest.mock('utils/api')
const mockFetch = fetchResource as jest.Mock

Finally I had access to things like fetchResource.mockResolvedValueOnce()

Collapse
terabaud profile image
Lea Rosema Author

Cool, learned something new today, thank you =)

Collapse
jstarmx profile image
James Starkie

Thanks very much for this, I was struggling to mock node-fetch in TypeScript and this was very helpful :)

Collapse
terabaud profile image
Lea Rosema Author

Awesome, glad it was helpful to you :)

Collapse
hantsy_26 profile image
Hantsy Bai

I tried to mock mongoose.connect in my Nestjs, but failed, stackoverflow.com/questions/629087...