Skip to main content

Testing

Testing Recoil state inside of a React componentโ€‹

It can be helpful to test Recoil state when testing a component. You can compare the new state against expected values using this pattern. It uses a React functional component, useRecoilValue and useEffect, to observe an atom/selector's changes and execute a callback every time the user performs an action that modifies the state.

export const RecoilObserver = ({node, onChange}) => {
const value = useRecoilValue(node);
useEffect(() => onChange(value), [onChange, value]);
return null;
};
  • node: can be an atom or a selector.
  • onChange: this function will be called every time the state changes.

Example: Form state modified by userโ€‹

Componentโ€‹

const nameState = atom({
key: 'nameAtom',
default: '',
});

function Form() {
const [name, setName] = useRecoilState(nameState);
return (
<form>
<input
data-testid="name_input"
type="text"
value={name}
onChange={event => setName(event.target.value)}
/>
</form>
);
}

Testโ€‹

describe('The form state should', () => {
test('change when the user enters a name.', () => {
const onChange = jest.fn();

render(
<RecoilRoot>
<RecoilObserver node={nameState} onChange={onChange} />
<Form />
</RecoilRoot>,
);

const component = screen.getByTestId('name_input');

fireEvent.change(component, {target: {value: 'Recoil'}});

expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith(''); // Initial state on render.
expect(onChange).toHaveBeenCalledWith('Recoil'); // New value on change.
});
});

Testing Recoil state with asynchronous queries inside of a React componentโ€‹

A common pattern for atoms is using asynchronous queries fetch the state of the atom, in a selector, or as part of an effect. This causes the component to be suspended. However, while testing, the component is suspended will not update in the DOM without acting. To test this scenario, we need a helper function:

// act and advance jest timers
function flushPromisesAndTimers(): Promise<void> {
return act(
() =>
new Promise(resolve => {
setTimeout(resolve, 100);
jest.runAllTimers();
}),
);
}

Example: Title with data returned from asynchronous data queryโ€‹

Componentโ€‹

const getDefaultTitleAtomState = async () => {
const response = await fetch('https://example.com/returns/a/json');
return await response.json(); // { title: 'real title' };
};

const titleState = atom({
key: 'titleState',
default: getDefaultTitleAtomState(),
});

function Title() {
const data = useRecoilValue(titleState);
return (
<div>
<h1>{data.title}</h1>
</div>
);
}

Testโ€‹

describe('Title Component', () => {
test('display the title correctly', async () => {
const mockState = {title: 'test title'};
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockState),
}),
);

render(
<RecoilRoot>
<Suspense fallback={<div>loading...</div>}>
<Title />
</Suspense>
</RecoilRoot>,
);
await flushPromisesAndTimers();

expect(screen.getByText(mockState.title)).toBeInTheDocument();
expect(screen.getByText('loading...')).not.toBeInTheDocument();
});
});

Testing Recoil state inside a Custom Hookโ€‹

Sometimes it is convenient to write custom React hooks that rely on Recoil state. These need to be wrapped in a <RecoilRoot>. The React Hooks Testing Library can help with this pattern.

Example: React Hooks Testing Libraryโ€‹

Stateโ€‹

const countState = atom({
key: 'countAtom',
default: 0,
});

Hookโ€‹

const useMyCustomHook = () => {
const [count, setCount] = useRecoilState(countState);
// Insert other Recoil state here...
// Insert other hook logic here...
return count;
};

Testโ€‹

test('Test useMyCustomHook', () => {
const {result} = renderHook(() => useMyCustomHook(), {
wrapper: RecoilRoot,
});
expect(result.current).toEqual(0);
});

Testing Recoil state outside of Reactโ€‹

It can be useful to manipulate and evaluate Recoil selectors outside of a React context for testing. This can be done by working with a Recoil Snapshot. You can build a fresh snapshot using snapshot_UNSTABLE() and then use that Snapshot to evaluate selectors for testing.

Example: Jest unit testing selectorsโ€‹

const numberState = atom({key: 'Number', default: 0});

const multipliedState = selector({
key: 'MultipliedNumber',
get: ({get}) => get(numberState) * 100,
});

test('Test multipliedState', () => {
const initialSnapshot = snapshot_UNSTABLE();
expect(initialSnapshot.getLoadable(multipliedState).valueOrThrow()).toBe(0);

const testSnapshot = snapshot_UNSTABLE(({set}) => set(numberState, 1));
expect(testSnapshot.getLoadable(multipliedState).valueOrThrow()).toBe(100);
});

Testing async selectorsโ€‹

When testing async selectors it is necessary to retain() the snapshot in order to avoid early cancelation.

const initialSnapshot = snapshot_UNSTABLE();
const release = initialSnapshot.retain();

try {

// your test

} finally {
release();
}

Clearing all selector cachesโ€‹

Selector caches are shared between <RecoilRoot>'s and tests, so you may need to clear the cache after each test.

const clearSelectorCachesState = selector({
key: 'ClearSelectorCaches',
get: ({getCallback}) => getCallback(({snapshot, refresh}) => () => {
for (const node of snapshot.getNodes_UNSTABLE()) {
refresh(node);
}
}),
});

const clearSelectorCaches = testingSnapshot.getLoadable(clearSelectorCachesState).getValue();

// Assuming we're in a file added to Jest's setupFilesAfterEnv:
afterEach(clearSelectorCaches);