この記事は、株式会社ゆめみの23卒 Advent Calendar 2023のアドベントカレンダーになります!!
色々な種類の記事が投稿されるので、お楽しみに🤗
はじめに
この記事はメンバーに知見がない状態から実際にテスト実装を行えるようになるまでの過程を記したものです。
僕たちのプロジェクトのコードにはテストがありませんでした。厳しいスケジュールの中で後回しにせざるを得なかったのです。
最近プロダクトの将来への不安と不便さが頂点に達したので、テストを導入することにしました。
メンバーの全面的な協力の元で試行錯誤した結果、知見があるメンバーがいない状態から1ヶ月でテスト実装前提のタスクを切ったり簡単なテストコードをお互いにレビューできるところまで来ました。
以下で取り組んだことを紹介していきます。
勉強会
自分含めメンバーに知見がなかったので、まず勉強会から始めました。朝のミーティング後の30分を使って毎日やりました。
最初は輪読会
「テスト何もわからん」という状態だったので、まずはこちらの書籍の輪読会をしました。
フロントエンド開発のためのテスト入門 今からでも知っておきたい自動テスト戦略の必須知識 吉井 健文 (著)
最初1~2週間くらいは愚直に本に従って進めた気がします。
飽きてモブプロへ
だんだん慣れてくると、実際のプロジェクトと本の乖離が気になってきたのでメンバーの声で既存のプロダクトコードのテスト実装のモブプロに移りました。執筆時も開催してます。
なんとなく慣れてきたところで実践に入ると勉強会を開きながら既存実装を積み上げていけるので良かったと思います。
また、チームには設計に詳しいベテランのメンバーがいたので毎回ディスカッションしつつ進めることができ、想像より学びが大きかったです。
タスクで参考にするための既存実装もここで作りました。
テスト戦略の決定
次はテスト戦略です。
勉強会の中でテスト戦略の相談もすることで、設計に詳しいベテランメンバーにまたまた救ってもらいつつ方針を決めていきました。
その結果サクッと導入できることを最優先として以下の方針を取りました。
Testing Trophyに従う
Kent C. Dodds氏が提唱するTesting Trophyに従って統合テストを厚めに、必要に応じて単体テストを書いてます。一度にたくさんのことをやりすぎると中途半端になってしまう気がしたのでE2Eは見送りました。
結合テストを行っていたのもあって統合テストのイメージは掴みやすかったのも追い風になったと思います。
MSWを活用する
モックサーバーとしてMSWを使っていたのでこれを活かすことにしました。
また、モックサーバー用のディレクトリはすでに以下のように作られていましたが、
src/api-mocks/{ 個々のモックされたエンドポイント用のディレクトリ }
完全にモックサーバー用に特化したディレクトリだったので、テストの実装とは分けました。
もしテストでエラー時のレスポンスなども確かめたくなった時は以下のようなモックサーバー作成用の純粋関数を活用しています。
定義側
type mockHandlerProps = {
response?: any
status?: number
};
export const createMockHandler = ({ path }: { path: string }) => {
const url = path;
const mockHandler = ({ response, status }: mockHandlerProps) =>
rest.get(url, (_, res, ctx) => {
return res(ctx.json(response), ctx.status(status ?? 200));
});
return { mockHandler };
};
呼び出しのイメージ
const { mockHandler: mockUsersResponse } = createMockHandler({
path: '/sample-users',
});
// httpクライアントライブラリをモック
const { queryWrapper: wrapper } = createQueryWrapper();
describe('画面の出しわけ', () => {
test('読み込み時にローディングが表示される', async () => {
render(<UsersTable />, { wrapper });
expect(screen.getByText('ローディング')).toBeInTheDocument();
});
describe('リクエスト成功時', () => {
// 正常系なのでモックサーバーのレスポンスをそのまま使う
test('データが存在する場合、テーブルにデータが表示される', async () => {
render(<UsersTable />, { wrapper });
expect(await screen.findByText('test-user-id')).toBeInTheDocument();
});
test('データが空の場合、データが空であることを示すテキストを表示', async () => {
// ここでモックサーバーのレスポンスを上書き
server.use(mockUsersResponse({ response: emptyResponse }));
render(<UsersTable />, { wrapper });
expect(
await screen.findByText('データが存在しません。'),
).toBeInTheDocument();
});
});
test('リクエスト失敗時、エラーが表示される', async () => {
// ここでモックサーバーのステータスを上書き
server.use(mockUsersResponse({ status: 500 }));
render(<UsersTable />, { wrapper });
expect(
await screen.findByText('データの取得に失敗しました'),
).toBeInTheDocument();
});
Storybookはテストには使わない
play関数などを使えばStoryとテストが対応するので、便利らしいのですがそもそもStoryをちゃんと作ってないのとテストコード内でテストに関することを完結させた方が初心者にはわかりやすいと判断してStorybookはテストに活用してません。
他にも色々と細かいことは決めましたが、、
思い出せないので割愛します
実際にテストコードを含んだチケットを切ってみる
テスト勉強会でモブプロを重ねたことで、気づけば既存実装を頼りにすればテストが書ける状態になっていました。
参考になる実装がプロジェクト内に存在するような状態を意識した上でテスト実装を含むタスクを切っています。
思わぬ発見
画面ごとに手動で動作確認するのを嫌って統合テストをガッツリ書くためにテストを導入したと僕は思っているのですが、思わぬ収穫がありました。
単体テストを書くことでタスクの単位を細かくできることです。
具体な例なのですが、プロジェクトではダイアログコンポーネントをrender hooks pattern
で実装しています。
テスト導入以前はダイアログ描画フックの実装だけをチケットとして切り出すことができず、画面実装に混ぜ込む形になっておりタスクが大きくなっていました。
しかしテストでカスタムフック単位で動作確認できるようにすることでダイアログ実装だけ小さいタスクとして分割できるようになったので、タスクが切りやすくなりました。
最後に
テストケース1つすらまともに動かず格闘し続けて終わる回が3回くらい続いても見捨てずに参加してくれたチームの皆さんに心の底から感謝を伝えたいと思います。
皆さんのおかげでテストコードを書く前提のチケットを切れてます!!!
ということで、皆さんも良いテストライフを!