Skip to main content
Version: 7.3

Unit testing hooks

danger

Be careful when using jest.mock on modules like rest-hooks. Eliminating expected exports can lead to hard-to trace errors like TypeError: Class extends value undefined is not a function or null.

Instead either do a partial mock, or better mockResolvedValue on your endpoints.

Hooks allow you to pull complex behaviors out of your components into succinct, composable functions. This makes testing component behavior potentially much easier. But how does this work if you want to use hooks from rest-hooks?

We have provided some simple utilities to reduce boilerplate for unit tests that are wrappers around @testing-library/react-hooks's renderHook().

We want a renderRestHook() function that renders in the context of both a Provider and Suspense boundary.

These will generally be done during test setup. It's important to call cleanup upon test completion.

caution

renderRestHook() creates a Provider context with new manager instances. This means each call to renderRestHook() will result in a completely fresh cache state as well as manager state.

Polyfill fetch in node

Node doesn't come with fetch out of the box, so we need to be sure to polyfill it.

npm install --saveDev  whatwg-fetch

Jest

// jest.config.js
module.exports = {
// other things
setupFiles: ['./testSetup.js'],
};
// testSetup.js
require('whatwg-fetch');

Example:

import nock from 'nock';
import { makeRenderRestHook } from '@rest-hooks/test';
import makeCacheProvider from '@rest-hooks/react/makeCacheProvider';

describe('useSuspense()', () => {
let renderRestHook: ReturnType<typeof makeRenderRestHook>;

beforeEach(() => {
nock(/.*/)
.persist()
.defaultReplyHeaders({
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
})
.options(/.*/)
.reply(200)
.get(`/article/0`)
.reply(403, {});
renderRestHook = makeRenderRestHook(CacheProvider);
});

afterEach(() => {
nock.cleanAll();
});

it('should throw errors on bad network', async () => {
const { result, waitFor } = renderRestHook(() => {
return useSuspense(ArticleResource.get, {
title: '0',
});
});
expect(result.current).toBeUndefined();
await waitFor(() => expect(result.current).toBeDefined());
expect(result.error).toBeDefined();
expect((result.error as any).status).toBe(403);
});
});
import nock from 'nock';
import { makeRenderRestHook } from '@rest-hooks/test';
import makeCacheProvider from '@rest-hooks/redux/makeCacheProvider';

describe('useSuspense()', () => {
let renderRestHook: ReturnType<typeof makeRenderRestHook>;

beforeEach(() => {
nock(/.*/)
.persist()
.defaultReplyHeaders({
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
})
.options(/.*/)
.reply(200)
.get(`/article/0`)
.reply(403, {});
renderRestHook = makeRenderRestHook(makeCacheProvider);
});

afterEach(() => {
nock.cleanAll();
});

it('should throw errors on bad network', async () => {
const { result, waitFor } = renderRestHook(() => {
return useSuspense(ArticleResource.get, {
title: '0',
});
});
expect(result.current).toBeUndefined();
await waitFor(() => expect(result.current).toBeDefined());
expect(result.error).toBeDefined();
expect((result.error as any).status).toBe(403);
});
});