Using jest.mock is the opposite of TDD

— 5 minute read

Using jest.mock is an after-thought. It is the opposite of a test-driven mentality. It says that the code is so untestable that you need to use a HACK based on a compile-time transformation to test your code. (This hack also is incompatible with ESM).

In a TypeScript based project, it is not type-safe, and does not alert you to incompatibilities in your test code. It is actively dangerous and helps cover up issues in your codebase.

The paths passed to jest.mock are not simple to refactor, nor easy to maintain.

This is also true for monkey-patching global objects. (think fetch, window.* , etc.)

There is another way!

1) Describe your dependencies as interfaces

type Widget = { id: string };

type HttpClient = (
input: RequestInfo | URL,
init?: RequestInit
) => Promise<Response>;

interface WidgetClient {
getWidget(id: string): Promise<Widget>;
saveWidget(widget: Widget): Promise<void>;
}

2) Write a "Real" implementation

Note how this takes a HttpClient. We should also unit test this ApiClient using a fake HttpClient implementation.

    
class WidgetApiClient implements WidgetClient {
constructor(private httpClient: HttpClient = window.fetch) {}
getWidget(id: string) {
return this.httpClient("/url/" + id)
.then((res) => res.json())
.then(({ data }) => data as Widget);
}
saveWidget(widget: Widget) {
return this.httpClient("/url" + widget.id, {
method: "PUT",
body: JSON.stringify(widget),
}).then(() => undefined);
}
}

3) Write a "Fake" implementation that provides the functionality required by your test

It might be a good idea to write some unit tests for your fake to prove that it behaves similarly to your backend.

    
class WidgetFakeClient implements WidgetClient {
constructor(private widgetStore: Record<string, Widget> = {}) {}
getWidget(id: string) {
return Promise.resolve(this.widgetStore[id]!);
}
saveWidget(widget: Widget) {
this.widgetStore[widget.id] = widget;
return Promise.resolve();
}
}

4) Pass these dependencies as defaulted parameters to your code, that can then be overridden in tests

    
const Widgets = (props = { widgetClient = new WidgetApiClient() }) => {
...
}

Now you've got a fake that you can use alongside your test, or in cases where the api is unstable. You have also isolated http into a layer in your application and can isolate your tests to only include downstream.