Skip to content

Instantly share code, notes, and snippets.

@lebbe
Last active January 4, 2023 14:11
Show Gist options
  • Save lebbe/31537027f6c45198e97798da23c8c143 to your computer and use it in GitHub Desktop.
Save lebbe/31537027f6c45198e97798da23c8c143 to your computer and use it in GitHub Desktop.
Mock latency without setTimeout

Mock latency without setTimeout

I needed to test the presence of a "loading-indicator" in a widget that posted data. Usually I would have done this with window.setTimeout or something similar: The timeout simulates the time it takes to complete the network call. But I find that running the whole test suite for my application takes a long time, and I wanted to avoid these arbitrary timeout lengths.

Instead I wanted more control of when the loading is done: What if I could hold onto the "loading"-phase in the application until the exact moment I was done checking the presence of the loading-indicator? In Jest, I want to simply tell it that loadingIsDone() and then continue testing that application state is correct after loading.

Therefore I made this simple utility. When calling mockLatency, it returns a function startLoading and a resolver completeLoading.

startLoading I typically feed to the mocked implementation of axios, and when I am done testing the "loading phase" of the application, I can call completeLoading and test the "done" phase of the application.

This means no more of this shenanigans:

mockedAxios.post.mockImplementationOnce(new Promise(r => setTimeout(r, 300)));

// TEST LOADING PHASE HERE

await act(async function() {
  // Wait for be sure the 300ms has ellapsed before testing further
  await new Promise(r => setTimeout(r, 300))
});

// TEST AFTER LOADING HERE

Instead you can do like this:

let { completeLoading, startLoading } = mockLatency();
mockedAxios.post.mockImplementationOnce(startLoading);

// TEST LOADING PHASE HERE

await act(async function() {
  await completeLoading(1);
});

// TEST AFTER LOADING HERE

As you can see, this saves 0 lines of code, but I would say that the code is somewhat more readable, and I know by experience that my tests runs faster.

/**
* Lets you mock latency in a very controlled manner. You feed startLoading Promise to the mocker,
* and when you want the promise to actually be resolved, you manually call resolver. This
* prevents us from using setTimeout and similar in Jest tests.
*/
function mockLatency() {
let resolver: (value: unknown) => void;
function completeLoading(value: unknown) {
resolver(value);
}
function startLoading() {
return new Promise((resolve) => {
resolver = resolve;
});
}
return { completeLoading, startLoading };
}
// EXAMPLE CODE
import { render, screen } from '@testing-library/react';
import SomeWidget from './SomeWidget';
import axios from 'axios';
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('A component that loads something with axios', function () {
test('Loading wheel is present while the component is posting data', async function () {
let { completeLoading, startLoading } = mockLatency();
mockedAxios.post.mockImplementationOnce(startLoading);
render(<SomeWidget />);
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(screen.getByTestId('loadwheel')).toBeInTheDocument();
await act(async function () {
completeLoading(1); // Feed a response here if you need.
});
expect(screen.queryByTestId('loadwheel')).toBeNull();
});
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment