The ability to provide a mock implementation of functionality is essential to unit testing, however the method for doing so with Jest and TypeScript is not well documented. This tutorial aims to elucidate the method for mocking a JavaScript library that is loaded into your TypeScript project. This is demonstrated with the ws
WebSocket library but applies equally to any library, including built-in NodeJS modules.
Applicability
Please note that the information here only applies to JavaScript modules that you wish to mock. If you wish to mock a TypeScript class (one defined in a *.ts
file as export default class MyClass { ... }
then the procedure is different.
Prerequisites
- NodeJS v18+
- TS Node: A TypeScript execution environment for NodeJS.
- Jest v29+: The unit testing framework.
- TS Jest: Allow Jest to test projects written in TypeScript.
- Jest types: TypeScript type definitions for Jest functions.
Note that I will be using ts-jest
instead of installing Babel manually. The requirements can be installed with:
npm install --save-dev @jest/globals @types/jest jest ts-jest ts-node
The library I will be mocking is the WebSocket library ws
. Modify the examples to suit whichever library you are using.
# Install the WebSocket library.
npm install ws
# ws is JavaScript so type annotations for TypeScript must be installed separately.
npm install --save-dev @types/ws
You will need to configure jest by using the creating the file jest.config.js
as described in the ts-jest documentation:
npx ts-jest config:init
You will also need a TypeScript configuration file tsconfig.json. Here is mine for reference:
{
"compilerOptions": {
"lib": [
"es2023"
],
"module": "Node16",
"target": "ES2022",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "Node16",
"esModuleInterop": true,
"noImplicitAny": false
}
}
Finally: create your test file as tests/my.test.ts
. You can then run the test using npx jest
.
TL;DR: A Full Example
// Import your library using "import * as ...".
import * as ws from 'ws';
// Tell Jest to mock all the functions and classes in the library.
jest.mock('ws');
// This is how to access a mocked class.
// This lets you do things like count the number of calls to the constructor.
const MockWebSocket = ws.WebSocket as jest.MockedClass<typeof ws.WebSocket>;
MockWebSocket.mockClear();
// This is how to create a mock object.
const socket = new ws.WebSocket('') as jest.MockedObjectDeep<ws.WebSocket>;
// Set the implementation of a mocked function.
socket.close.mockImplementation(() => console.log('Closed the socket'));
Detailed Explanation
Import
The first step is to import your library. In classic NodeJS you would have written const ws = require('ws')
. In TypeScript you must write import * as ws from 'ws'
. Do not forget the * as
part. This tells it to import everything from the module and make it accessible as properties of the object ws
.
Replace the Module with a Mock
Next, tell Jest to replace everything in the library with mock implementations using jest.mock('ws')
. Note that this function call gets hoisted to the top of the file, so it actually runs before the import
statement. It hooks into the import
call and returns mock items instead of real items.
Access the Mock Constructor
const MockWebSocket = ws.WebSocket as jest.MockedClass<typeof ws.WebSocket>;
MockWebSocket.mockClear();
Here, ws.WebSocket
is the constructor function for the WebSocket
class. TypeScript does not know that its implementation was replaced when jest.mock('ws')
hooked the import
statement, so we must tell it by casting it to jest.MockedClass<typeof ws.WebSocket>
. This cast maintains the constructor function’s original signature and augments it with Jest’s mocking functions such as mockClear()
and mockImplementation()
. The result is saved into MockWebSocket
.
It should be noted that although MockWebSocket
is itself a constructor function, its return type is WebSocket
, not MockWebSocket
. This means that you cannot instantiate a MockWebSocket
by using new MockWebSocket
.
Instantiate a Mock Object
const socket = new ws.WebSocket('') as jest.MockedObjectDeep<ws.WebSocket>;
To create a mock object use new
to create the object as normal, and then cast it to a MockedObjectDeep
. As with accessing the constructor, new ws.WebSocket
actually constructs a mocked WebSocket
due to jest.mock()
having swapped out the implementation of the constructor during the import, but TypeScript doesn’t know this and so the cast is required to tell it.
Implement a Mock Function
To add functionality to your mock object you can use Jest’s mockImplementation()
function. For example, we may wish to ensure that send()
is not called after close()
.
To accomplish this we will add a new property to the mock called isClosed
, and provide implementations for send()
and close()
.
// Extend the mocked WebSocket to give it an 'isClosed' property.
type MockedSocket = jest.MockedObjectDeep<ws.WebSocket> & { isClosed: boolean }
// Create the mock socket.
const socket = new ws.WebSocket('') as MockedSocket;
socket.isClosed = false;
// Implement the close() and send() functions.
socket.close.mockImplementation(() => socket.isClosed = true);
socket.send.mockImplementation(() => { if (socket.isClosed) throw new Error('Sending after closing'); });
// Run the code under test, using the mock socket.
// testMyCode(socket);
Constructor Parameters
It is slightly unfortunate that the mock class constructors take the same parameters as the real class constructors. This means that you must pass valid and correctly typed arguments to your mock object constructors, even though they will not be used. This can result in having to create a lot of unnecessary mocks as you need to mock the class’s dependencies, the dependencies’ dependencies, and so on.