自動テストを実装してみた
はじめに
自動テストの基礎知識についてはここでは述べません。テストコードを書くときに注意したことについて、まとめました。
環境
ライブラリ・フレームワーク | バージョン |
---|---|
TypeScript | v5.2.2 |
React.js | v18 |
Next.js | v14.0.4 |
jest | v29.7.0 |
testing-library/react | v14.1.2 |
テストコード
まずJestで自動テストを書くとき、以下をテンプレートとして実装しました。
書き方を統一するのは、可読性を良くし、コードの理解を早めることができます。
// モック関数
const mockRegister = jest.fn();
// テストグループ
describe('~/components/container/Form/Date.tsx', () => {
// モックデータ
const mockProps = {
labelName: 'testLabel',
errorMessage: undefined,
register: mockRegister(),
};
// ライフサイクル
beforeEach(() => {
cleanup();
mockRegister.mockReset();
});
// テスト
it('ラベル名を表示する', async () => {
const { getByText } = render(
<Date
presentational={{
labelName: mockProps.labelName,
errorMessage: mockProps.errorMessage,
register: mockProps.register,
}}
/>,
);
const labelElement = getByText(mockProps.labelName);
expect(labelElement).toHaveTextContent(mockProps.labelName);
});
));
具体的なテストコードの内容(例えば、<Date>
など)は、特に気にしないでください。
テストを書く時に考えたこと
どのようなテストを書いたら良いのか
我々実装者が保証すべきことはホワイトボックステストであり、コードの動作を保証することをテストコードで伝えることができれば良いです。
そのため、テスト名は「何を」テストしているのかを簡潔に説明できていることが重要です。
テストコードを書くときは、各テストが独立して実行されるようにしましょう。
これによって、各テストがクリーンな状態で開始され、副作用を受けないようになります。
何をテストしたら良いのか
あるロジックに対してテストを作成するとき、コードの動作についてのフローチャートを考えると分かりやすいです。
さらに、Unit テストを書くときは以下の点について、考慮するようにしましょう。
- 全てが正しく動いたと仮定して、理想的な条件下でメソッドはどのように動作するのか
- 特殊な条件やエッジケースはあるのか
- 例外が起きた時に、このメソッドはエラーを起こすのか
- ロジックの流れや条件分岐が、適切に動いているのか
※ 静的解析ツールや TypeScript を導入している場合は、型エラーがコンパイルエラーとして表示されます。なので、指定された型通りに値を渡せているかどうかはこのツールに任せ、Unit テストとしてはテストの対象に含めない、というルールにしても良いかもしれないです。
そして、最終的に書いたテストが十分かどうかは、以下の 3 点を指標にして判断します。
- まずは、正しく動作することを保証しているか
- 壊れる可能性のある箇所はすべてテストしているか(もちろん全ては無理なので、よくあるケースやエッジケースなどを考慮できているか)
- テストファーストで考えられているか
テストコードを書く時に考えたこと
モックの初期値
モックしようとしているものが変数の場合、初期値を設定するかしないかを考える必要がありました。
初期値を設定し、その初期値でテストをする場合に再度テストコードを記載する必要がなくなるため、記述量が少なくなります。
また、条件が複数のテストで共通している場合、重複コードを減らし冗長性が無くなります。
しかし、各々のテスト間は独立しているべきで、モック変数はテストごとにリセットされるかもしれません。そのため、初期値は設定せず各テストごとに値を指定し直した方が良いかと思います。
さて、Jestでの初期値の実装方法ですが、jest.fn()
にはmockImplementation
とmockReturnValue
メソッドがあります。
const mockPush = jest.fn();
const mockIsAuth = jest.fn();
const mockUseRouterNav = jest.fn().mockImplementation(() => ({
push: (...arg: unknown[]) => mockPush(...arg),
}));
const mockUseSignin = jest.fn().mockImplementation(() => ({
isAuth: mockIsAuth(),
}));
jest.mock('next/navigation', () => ({
useRouter: () => mockUseRouterNav(),
}));
jest.mock('~/hooks/useSignin', () => ({
useSignin: () => mockUseSignin(),
}));
describe('~/components/container/Header/Header.tsx', () => {
let user: UserEvent;
it('ユーザーアイコンをクリックして、トップ画面に遷移する', async () => {
mockIsAuth.mockReturnValue(true);
mockPathname.mockReturnValue(PAGE_PATH.COMPLETED);
const { container } = render(<Header />);
const userElement = container.querySelector('#UserCircle');
expect(userElement).toBeDefined();
await user.click(userElement!);
await waitFor(() => {
expect(mockPush).toHaveBeenCalled();
expect(mockPush).toHaveBeenCalledWith(PAGE_PATH.TOP);
});
});
});
mockReturnValue
は、mockしている変数をjest.fn()
でモックし、これを実行したときに返却される値を設定します。そのため、変数をモックしたい場合に使います。
一方で、mockImplementation
は、mockしようとしている関数に返り値やメソッド、フィールドもモックしたい場合に用います。
モックの初期化
モックを初期化する場合、mockReset
を使用します。このとき、定義自体が削除されてしまうため、条件によって値が変わるようなものだけをmockReset
します。
コンポーネントをモックする場合、基本的には各テストごとにレンダリングした方が良いです。これは、各テストを独立させるためです。しかし、そのコンポーネント内のすべてにおいてモックする必要がなければ、共通化して使用しても良いかもしれないです。
コード
実際のコードは、以下のリポジトリを参照してください。
転載元
Discussion