Setting up unit tests in a TypeScript and Node.js project can significantly improve code quality and developer confidence. This guide, is going through configuring Jest for unit testing in a TypeScript + Node.js environment, covering essential best practices and how to apply test-driven development (TDD). Whether a backend service or a full-stack app, it should help with how to write clean, maintainable, and reliable tests that integrate smoothly into your development workflow.
Unit: method, class, module
Types of tests: unit, integration, E2E (end-to-end, UI)
Setup Project
Create NodeJs Project:
npm init -y
Install dependencies:
npm i –save-dev typescript jest ts-jest @types/jest ts-node
Generate tsconfig:
npx tsc –init
To avoid warnings set “esModuleInterop”: true
Create jest config file:
npx ts-jest config:init
Previous will create javascript jest config file (jest.config.js). Since we want all in typescript we can delete and create typescript jest config: jest.config.ts
//basic jest.config.ts
import type { Config } from '@jest/types';
const config: Config.InitialOptions = {
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true,
}
export default config;
Writing tests
AAA principle
- Arrange
- Act
- Assert
👉Good practice is to write only 1 assert per test to be able to identify what exactly has failed in the tested functionality!
// sample test
import { toUpperCase } from "../app/util";
describe('util test suite', () => {
it('should return uppercase of valid string', () => {
// arrange
const expected = 'ABCD';
// act
const actual = toUpperCase('abcd');
// assert
expect(actual).toBe(expected);
});
});
Some most used ‘expect’ methods
.toBe
compare primitive types (string, number, …)
.toEqual
compare objects
.toHaveLength
compare array length
.toContain
check if array contains an element
// To check more elements in array
const someArrayOfChars = ['a', 'b', 'c'];
expect(someArrayOfChars).toEqual(
expect.arrayContaining(['b', 'c', 'a'])
);
.not
negate the matcher: expect(value).not.toBe(1)
Parametrized tests
In case we need to run the test for different parameters (inputs and results), but test itself will stay the same, it’s not necessary to repeat, but instead parametrized test can be written.
it.each([
{input: 'abc', expected: 'ABC'}
{input: 'my-string', expected: 'MY-STRING'}
{input: 'xyz', expected: 'XYZ'}
])('$input toUpperCase should be $expected',
({input, expected}) => {
const actual = toUpperCase(input);
expect(actual).toBe(expected);
});
Testing for Errors
// three different ways how to test for errors
// 1. function
it('should throw error invalid argument', () => {
function expectError() {
const actual = toUpperCase('');
}
expect(expectError).toThrow();
// or
expect(expectError).toThrowError('Invalid arg...');
})
// 2. arrow function
it('should throw error invalid argument', () => {
expect(() => {
const actual = toUpperCase('');
}).toThrowError('Invalid arg...');
})
// 3. try-catch block
it('should throw error invalid argument', (done) => {
try {
toUpperCase('');
//a) need to fail the test otherwise it be OK even if the error is not thrown
fail('should throw error');
//b) jest has issue and throws 'fail is not defined' => workaround with 'done'
done('should throw error');
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error).toHaveProperty('message', 'Invalid argument');
done();
}
})
Test doubles
- stubs
- fakes
- mocks
- spies
Stubs
Usual representation is an incomplete structure from the original and is filled only with minimum necessary data required for test.
Fakes
It’s e.g. empty function definition for a callback in case I don’t really need it to be called: () => {}
test('use fake callback', () => {
const actual = someFunctionWithClbk('', () => {});
expect(actual).toBe('');
})
Mocks
Used when Fakes are not enough and we need to know more information: how many times it’s called, call parameters, define return value, etc.
const clbkMock = jest.fn();
afterEach(() => {
// mocks need to be reset
jest.clearAllMocks();
})
test('', () => {
const actual = someFceWithClbk('', clbMock);
expect(clbMock).toBeCalledWith('<arguments>');
expect(clbMock).toBeCalledTimes(1)
})
Spies
Spies
- Spies, not like mocks, aren’t directly injected into our system-under-test,
- original functionality is preserved,
- usually track method calls
- works on classes, modules
describe('spy example', () => {
let sut: MyClass;
beforeEach(() => {
sut = new MyClass();
})
test('simple spy', () => {
const methodSpy = jest.SpyOn(sut, 'method');
sut.method('param');
expect(methodSpy).toBeCalledWith('param');
})
test('spy w/ change implementation', () => {
const methodSpy = jest.SpyOn(sut, 'method')
.mockImplementation(() => {
// own test implementation of 'method'
});
sut.method('param');
expect(methodSpy).toBeCalledWith('param');
})
})
Mocking modules
// 1. mock whole module
jest.mock('../app/.../someModule');
// 2. mock only what is necessary from module
jest.mock('../app/.../someModule', () => ({
// include all what needs to be kept
...jest.requireActual('../app/.../someModule'),
// mock only what is required
someFce: () => { return 'value'},
// or
someFce2: jest.fn()
}));
// 3. mock some libs (e.g.: uuid)
jest.mock('uuid', () => ({
v4: () => '123'
}));

Leave a Reply