Unit testing hooks
Do not use jest.mock on any rest-hooks library. This will likely result in hard-to trace
errors like TypeError: Class extends value undefined is not a function or null
.
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.
To support both providers, you must choose among two provider-generators to send as args to the renderRestHook() generator.
These will generally be done during test setup. It's important to call cleanup upon test completion.
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
- Yarn
yarn add --dev whatwg-fetch
npm install --saveDev whatwg-fetch
Jest
// jest.config.js
module.exports = {
// other things
setupFiles: ['./testSetup.js'],
};
// testSetup.js
require('whatwg-fetch');
Example:
- CacheProvider
- ExternalCacheProvider
import nock from 'nock';
import { makeRenderRestHook, makeCacheProvider } from '@rest-hooks/test';
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, waitForNextUpdate } = renderRestHook(() => {
return useSuspense(ArticleResource.detail(), {
title: '0',
});
});
expect(result.current).toBeUndefined();
await waitForNextUpdate();
expect(result.error).toBeDefined();
expect((result.error as any).status).toBe(403);
});
});
import nock from 'nock';
import { makeRenderRestHook, makeExternalCacheProvider } from '@rest-hooks/test';
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(makeExternalCacheProvider);
});
afterEach(() => {
nock.cleanAll();
});
it('should throw errors on bad network', async () => {
const { result, waitForNextUpdate } = renderRestHook(() => {
return useSuspense(ArticleResource.detail(), {
title: '0',
});
});
expect(result.current).toBeUndefined();
await waitForNextUpdate();
expect(result.error).toBeDefined();
expect((result.error as any).status).toBe(403);
});
});