本文へスキップ

テスト

Reactコンポーネント内のRecoil状態のテスト

コンポーネントをテストする際に、Recoilの状態をテストすることは役に立ちます。このパターンを使用して、新しい状態を期待値と比較することができます。これは、React関数コンポーネント、`useRecoilValue`、`useEffect`を使用して、`atom`/`selector`の変更を観察し、ユーザーが状態を変更するアクションを実行するたびにコールバックを実行します。

export const RecoilObserver = ({node, onChange}) => {
const value = useRecoilValue(node);
useEffect(() => onChange(value), [onChange, value]);
return null;
};
  • node: atomまたはselectorです。
  • onChange: 状態が変更されるたびに呼び出される関数です。

例:ユーザーによって変更されたフォームの状態

コンポーネント

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>
);
}

テスト

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.
});
});

Reactコンポーネント内の非同期クエリを使用したRecoil状態のテスト

atomの一般的なパターンは、selector内、またはeffectの一部として、atomの状態を取得するために非同期クエリを使用することです。これにより、コンポーネントは中断されます。しかし、テスト中は、コンポーネントが中断されても、アクションを実行しないとDOMは更新されません。このシナリオをテストするには、ヘルパー関数が必要です。

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

例:非同期データクエリから返されたデータを含むタイトル

コンポーネント

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>
);
}

テスト

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();
});
});

カスタムフック内のRecoil状態のテスト

Recoil状態に依存するカスタムReactフックを作成することが便利な場合があります。これらは`<RecoilRoot>`でラップする必要があります。 React Hooks Testing Libraryはこのパターンに役立ちます。

例:React Hooks Testing Library

状態

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

フック

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

テスト

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

React外でのRecoil状態のテスト

テストのために、Reactコンテキストの外でRecoil selectorを操作して評価することは役に立ちます。これは、RecoilのSnapshotを使用することで実行できます。`snapshot_UNSTABLE()`を使用して新しいスナップショットを作成し、そのSnapshotを使用してselectorをテストできます。

例:Jestを使用したselectorのユニットテスト

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);
});

非同期selectorのテスト

非同期selectorをテストする際には、早期キャンセルを回避するために、スナップショットをretain()する必要があります。

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

try {

// your test

} finally {
release();
}

すべてのselectorキャッシュのクリア

Selectorキャッシュは`<RecoilRoot>`とテスト間で共有されるため、各テスト後にキャッシュをクリアする必要がある場合があります。

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);