08.そして脱却へ

悩んだ末にたどり着いたコードです。

スモークテスト

describe('アイテム検索・登録モーダル画面', () => {
  it('コンポーネント作成', () => {
    const modal = compile(
      '<item-registration-modal data="items"></item-registration-modal>'
    );

    expect(modal).toBeTruthy();
  });
});

コンポーネントがエラーなく作成できる事だけをテストします。 依存サービスや子コンポーネントはできるだけモックでない本物を使いますが 初期処理が足りずにエラーになる場合は、サービスそのものでなくサービスのメソッドなど一部だけをモックします。

ユニットテスト

describe('アイテム検索・登録モーダル画面 リスト表示', () => {

  function setup(items) {
    spyOn(searchService, ['search']);
    searchService.search.and.returnValue(Promise.resolve(items));

    return compile(
      '<item-registration-modal data="items"></item-registration-modal>'
    );
  }

  function assert(items, expected) {
    const modal = setup(items);
    modal.getElementById('searchText').value = 'カルテットコミュニケーションズ';
    modal.getElementById('searchButton').click();

    expect(modal.getElementsByClassName('item').length).toBe(expected.listSize);
    expect(modal.getElementsById('saveButton').attr('disabled') === 'disabled')
      .toBe(expected.disabled);
  }

  it('検索に2件該当', () => {
    const items = [fakeItem(100), fakeItem(101)];
    const expected = { listSize: 2, disabled: false };
    assert(items, expected);
  });

  it('何も該当しない', () => {
    const items = [];
    const expected = { listSize: 0, disabled: true };
    assert(items, expected);
  });
});

export function fakeItem(id) {
  return {
    id: id,
    name: 'fake',
  };
}

依存サービスや子コンポーネントは全てモックを使い、必要なモデルはフェイクを使います。

テストの目的を示した describe() は1ファイルにひとつしか書きません。上記の例だと「リスト表示」だけをテストします。

setup() は環境づくりに徹する

実際のテスト、それもコンポーネントを対象にしたテストの場合、依存サービスのモックや HTML のコンパイルなど事前準備だけで結構な行数を使います。これらを全て setup() にまとめます。

assert() は検証に徹する

ひとつのシナリオを実行し、結果を検証します。検証のための expect() はいくつでも呼び出していいルールとします。

it() はパターン列挙に徹する

前提条件と結果にパターンを持たせます。 assert() を呼び出す以外の事は何もしません。

フォルダ構造

component
├── item-registration-modal
│   ├── component.js
│   ├── component.html
│   ├── smork-test.js
│   └── tests
│         ├── list-test.js
│         ├── resoponse-test.js
│         └── search-test.js

ひとつのコンポーネントに対して、スモークテストがひとつ、ユニットテストが複数という構造にします。

ユニットテストは「リスト表示」「API レスポンスのパターン」「再検索」など小さなシナリオ単位でテストファイルを分けるようにします。

何がいいのか

スモークテスト大事

例えば子コンポーネントに必須パラメータを増やした時、モックでない本物を使ったテストが落ちればコンポーネント側の修正が必要な事に気づけます。逆に個々のユニットテストでは、テストに関係のないパラメータ増加が原因で大量にテストが落ちると修正してまわるのがとても大変です。そのために スモークテストはモックでない本物を使い、ユニットテストはモックを使う というように役割を分けているのです。

仕様の数だけユニットテストを作る

ファイル名にテストの目的を書くことで、後からとても探しやすくなります。またユニットテストが増える事でコンポーネントが抱える仕事の多さが一目瞭然になります。私はファイル数が 3 以上になった時にコンポーネント分割を検討するようにしています。

あちこち検証してもカオスになりにくい

assert() に検証を集める事で、サービスがコールされる事、ボタンが disabled になる事、リストに表示された件数が 2 件になる事、を一箇所に集める事ができます。もし assert() のシナリオや検証にそぐわないイレギュラーなパターンが出てきたら、ユニットテストのファイルを増やせば良いだけです。

異常パターンも列挙できる

it() を仕様の列挙でなく、前提条件と結果のパターンにする事により、正常パターンと異常パターンを混在させる事ができます。

テストコード以外に大事にしているポイント

テストを書くタイミング

私はプロダクションコードをざっくり書き終えてブラウザでの動作確認を一度通したタイミングでユニットテストを書き始めるようにしています。実運用でのコンポーネントは、非同期処理だったり、コンポーネント連携だったり、イベントハンドリングだったり、複雑な処理をしています。その全てを把握しつつテストから書き始めるのはとても難しく、そこは TDD にこだわらず人間の目を通したほうが早くて確実です。

レガシーなテストコードに逆らわない

過去に書いたカオスなテストに新たなコードを追加する場合、あえてそのままカオスなスタイルでコードを書くようにしています。後からコードを追加する時って、その仕様に関連する部分を見つけ出してざーっとコードを読みますよね?その時に全く別のファイルに別のスタイルのコードが書いてあると、とても読みづらく検索性も低くなってしまいます。

テストは7割の力で書く

テストコードを完璧に書かないといけないという思い込みは、時に開発を苦しくさせます。思い出しましょう、全パターン網羅のテストは地球が滅亡する頃に完了します。誰もが見て完璧だと思うテストは tc39 のあのレベルです。画面に青いボタンを追加した時に「背景色が青である事」って検証しないですよね?無意識にそれを無視しているように、必要なポイントで必要なテストが書ければ十分じゃないかと思うのです。体感的には「ほぼ満足」の8割でなく「ちょっと足りないかな」の7割の力で書くテストが、苦しくならない絶妙なバランスです。