Testing with Jest and TypeScript, the tricky parts

Lea Rosema (she/her) - Feb 17 '20 - - Dev Community

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
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player