May 29, 2023
10 mins read
今週、社内のメンバーから以下のリーダブルテストコードの記事を見せてもらった。 かなり自分の中でささったので、Jestの場合どうするかを考えてみた
リーダブルテストコード
過度なDRYを行わず、APIドキュメントだと思って書く
脳内メモリを消費させない“リーダブルなテストコード”の書き方
上記の参考記事を参考に、Jest(React)の場合、どのようにリーダブルテストコードを書くかを考えてみた。 今回は、UIコンポーネントとカスタムHookそれぞれのテストコードを書いていく。
参考の記事にはリーダブルテストコードを書くには、以下の様に書く必要がある記載している
- テストコードにおいて、過度なDRYは読みやすさの敵
- 賢くてロジカルなテストコードより、誰でも(非エンジニアでも)読める愚直なテストコードを
- 脳内メモリを使わないテストコードはリーダブル
- 変数をなくし、上から順番に見るだけでテストの意図がわかるようにする
- 実行可能なAPIドキュメントだと思ってテストコードを書こう(テストコードはプログラムじゃなくてドキュメント)
もっとリーダブルにするコツ
- 実際のユースケースに近いテストデータを使用する
- 「あああ」「テストテスト」「ユーザー1」みたいなテストデータはNG
- describe / context / itの説明を丁寧に書く
- 「it “適切な値を返す”」みたいな具体性のない説明はNG
- すべての情報が1画面に収まるテストコードが理想
- 上下スクロールが頻繁に発生したり、他のファイルが見に行かなきゃいけないのはNG
- DRY禁止はあくまで原則。適宜メリット・デメリットを天秤にかける。
- 「明確なメリットがあるDRY」や「可読性の損なわない抽象化」まで放棄するのはNG
上記を考慮して、React(Jest)のテストケースを書いていく
input
タグで入力した値firstName
とlastName
が、p
タグで表示されるようになっている。
// @ref: https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-let-you-share-stateful-logic-not-state-itself
import { useFormInput } from './useFormInput';
export const Form = () => {
const firstNameProps = useFormInput('');
const lastNameProps = useFormInput('');
return (
<>
<label>
First name:
<input type='text' name='firstName' {...firstNameProps} />
</label>
<label>
Last name:
<input type='text' name='lastName' {...lastNameProps} />
</label>
<p>
<b>Good morning, {firstNameProps.value} {lastNameProps.value}.</b>
</p>
</>
);
}
上記のテストコードの観点としては、firstNameとlastNameの入力値が、pタグで表示されるかどうかを確認するテストコードを書く。
import {render,screen} from "@testing-library/react";
import {Form} from "./Form";
import userEvent from "@testing-library/user-event";
const user = userEvent.setup()
test('FirstNameとlastNameの入力した値が、表示されること', async () => {
render(<Form />);
await user.type(screen.getByRole('textbox', {name: 'First name:'}), 'taro')
await user.type(screen.getByRole('textbox', {name: 'Last name:'}), 'yamada')
expect(screen.getByText('Good morning, taro yamada.')).toBeInTheDocument();
})
input
タグに入力する内容は、変数にしていない上記は比較的まだ小さいコンポーネントだが、変数を使わず、上から順番に見るだけでテストの意図がわかるようになっている。
上記のコンポーネントで使用しているカスタムHookuseFormInput
のテストコードを書いていく。
input
タグで使用する属性を作成するオブジェクトを生成するカスタムHookである。
import {ChangeEventHandler, useState} from 'react';
export const useFormInput = (initialValue: string) => {
const [value, setValue] = useState(initialValue);
const handleChange: ChangeEventHandler<HTMLInputElement> = (e)=> {
setValue(e.target.value);
}
return {
value: value,
onChange: handleChange
};
}
import {renderHook} from "@testing-library/react";
import {useFormInput} from "./useFormInput";
import {act} from "react-dom/test-utils";
import {ChangeEvent} from "react";
test('引数の初期値がuseStateのセットされているか', () => {
const {result} = renderHook(() => useFormInput('taro'))
expect(result.current.value).toBe('taro')
})
describe('handleChange', () => {
test('発火したときに、入力された値がstateにセットされるか', () => {
const {result} = renderHook(() => useFormInput('taro'))
act(() => {
result.current.onChange({target: {value: 'jiro'}} as ChangeEvent<HTMLInputElement>)
})
expect(result.current.value).toBe('jiro')
})
})
onChange
で取得するイベントオブジェクトは、実際に使用しているイベントオブジェクトを使用しているdescribe
を使用しているテストケースをドキュメント代わりで使用する視点を持つことで、テストのリーダブル性が上がることがわかった。
意識次第ですぐに実践できるので、ぜひ取り入れていく
リーダブルテストコード
過度なDRYを行わず、APIドキュメントだと思って書く
脳内メモリを消費させない“リーダブルなテストコード”の書き方