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
It generates a jest.config.js
file with:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
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();
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');
});
});
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';
},
};
Add this to your jest.config.js
configuration file:
module.exports = {
// ...
transform: {
"\\.(css|less|scss)$": "./jest/stub-transformer.js"
}
};
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;
}
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);
});