👻

「単体テストの考え方/使い方」を読んでフロントエンド単体テストコードの規約を作ってみた

2023/07/06に公開

はじめに

「単体テストの考え方/使い方」を読んだのを良い機会に、私が所属するフロントエンドチーム内の単体テストコードのコーディング規約を作ってみました。
別途読書感想文も書いています。
https://zenn.dev/kikolski/articles/ae6258e5a99d2a

本を読んで学んだこと

本を読んで以下のことを学びました。

  • リファクタリング耐性を高めることの重要さ
    • DOM構造を検証しない方が良い、程度の知識はありました。しかし、子コンポーネントごと変更するような大規模なリファクタリングに備えるほどの耐性までは意識していませんでした。
  • テストコードの保守性の重要さ
    • いままではとにかく量を増やし、カバレッジを高めればよいと考えていました

これまでのテストコードの問題点

  • テストケース名が技術寄り
    • 新規チーム参入者にわかりずらいし、自分自身がだいぶ前に書いたテストコードを読み解くのがしんどい
  • 虚無なテスト実装に時間がとられる
  • 開発者によってコーディングスタイルがバラバラ
    • プロダクションコードにはコーディング規約が以前から存在していました
  • リファクタリングでテストコードが壊れやすい
    • リファクタリングの際に子コンポーネントのインターフェースが変わるとテストが壊れてしまう

開発環境

  • TypeScript
  • Vue2
  • Jest
  • VueTestUtils

策定したコーディング規約

チームで作った単体テストコーディング規約をご紹介します。

テストケース名は非開発者でもわかるように心がける

本の3.4.1章で言及されている、テスト・メソッドに名前を付けるときの指針を参考にしました。
非開発者でもわかるように心がけると、非開発者だけでなく新規参入の開発者にもわかりやすくなります。ビジネス側の意図もくみ取りやすくなります。

具体的には以下を心がけます。

  • 日本語で書く
  • 技術的な用語(propsとかtrue/falseとか)を書かない
// 例

// 検索画面のテストコード
describe('検索ボタンをクリックしたとき', () => {
   test('ページ数を1にする', () => {});
   
   test('ソート条件を更新日時の降順にする', () => {});
};

AAAでテストコードを分ける

こちらは本の3.1.1章でおすすめされている方法です。
ほとんどのテストがの3つのフェーズで分割できます。

  • Arrange(テストの準備)
  • Act(テスト対象処理の実行)
  • Assert(対象処理結果の検証)

読み手がテストコードの構造を把握しやすくなります。

  • 3つのフェーズそれぞれが空行を必要としない場合、3つをスペースで分ける。
// 例

const wrapper = mount(SomeComponent);

wrapper.findComponent({ name: 'btn' }).vm.emit('click');
await nextTick();

expect(wrapper.findComponent({ name: 'txt' }).text()).toBe('クリックされました');
  • 3つのフェーズいずれかが空行を必要とする場合、3つを空行で分け、さらにコメントでフェーズを明示する。
// 例

// Arrange
await router.push({ name: 'next-page' });

const wrapper = mount(SomeComponent);


// Act
wrapper.findComponent({ name: 'pagination' }).vm.emit('click', 4);
await nextTick();

wrapper.findComponent({ name: 'btn' }).vm.emit('click');
await nextTick();

// Assert
expect(wrapper.findComponent({ name: 'txt' })).toBe('クリックされました');

describeとtestは空行で区切る

見やすさ向上のためです。

// 例

describe('describe1', () => {
    test('test1', () => {});
    
    test('test2', () => {});
});

describe('describe2', () => {
    test('test1', () => {});
    
    test('test2', () => {});
});

1つのテストケースで1つの事柄のみをテストする

こちらは本の3.1.2章でおすすめされている方法です。
この規約はテスト内容を把握しやすくするためのものです。また、あるテストが失敗したときに何が失敗原因なのか把握しやすくなります。

取るに足らないテストを書かない

本の7.1章には以下4つのコードの分類が示されています

  • ドメイン・モデル/アルゴリズム
  • 過度に複雑なコード
  • 取るに足らないコード
  • コントローラ

このうち「ドメイン・モデル/アルゴリズム」、「過度に複雑なコード」には、退行を防ぐため、リファクタリングするためにテストコードを書くべきです。「コントローラ」のテストコードは統合テストに分類されるので今回は言及していません。
「取るに足らないコード」はテスト対象にすることを避けるべき、と本で言及されています。テストを書いても労力に見合った価値を得ることはできないからです。

取るに足らないテストの例

  • テスト対象コンポーネントのpropsで渡した文字列が表示されるか確認するテスト
  • テスト対象コンポーネントのpropsで渡した文字列が子コンポーネントのpropsに渡されているか確認するテスト
  • slotに入れた文字列が表示されるかを確認するテスト

なるべくテスト対象の子コンポーネントとのコミュニケーションのテストをしないようにする なるべく最終出力結果または、状態でテストする

本の6.2.2章に書かれている内容を参考にしました。コンポーネント間のコミュニケーションはリファクタリング耐性がありません。コミュニケーションは最終成果物ではなく、実装の詳細だからです。実装の詳細をテストしてしまうと、リファクタリングの際にテストが容易に壊れてしまいます。コンポーネントが出力するテキスト内容で検証するのが一番リファクタリング耐性が高いと考え、この規約を作りました。コンポーネントの出力するテキスト内容はVueTestUtilsだとこのメソッドで得られます。

ただし、外部ライブラリのコンポーネントとのコミュニケーションのテストは例外で可としています。理由としては、外部ライブラリは様々なプロダクトから参照されており、頻繁にインタフェースが変化することはありません。テストコードが壊れる原因になることは少ないです。また、外部ライブラリのコンポーネントの中を覗いて検証するのは工数的に厳しいと感じたのも理由です。

子コンポーネントとのコミュニケーションのテストの例

  • 子コンポーネントのメソッドが呼び出されているかどうかのテスト
  • 子コンポーネントのpropsを確認するテスト
  • 子コンポーネントがイベントをemitしてるかどうかを確認するテスト

最後に

フロントエンド単体テストのコーディング規約を紹介させていただきました。しかし、まだまだ規約の完成には程遠い思っています。実際に開発を通じてブラッシュアップしていきたいです。

Discussion