02.改善の第一歩

テストの書き方について、手探りでいくつかの「ちょっとした改善」を試みました。 が、私のちょっとした改善はそのどれもが「どこにテストを追加したらいいのか」問題を解決できるものではなく、結果的にはバッドプラクティスでした。

1)グルーピングするパターン

describe('画面の初期表示状態', ...);

describe('検索ボタンを押す', () => {
  describe('1回目の検索', ...);
  describe('2回目の検索', ...);
});

describe('検索サービスが呼ばれる', () => {
  it('1回目', ...);
  it('1回目:異常終了、2回目:正常終了', ...);
  it('2回とも正常終了', ...);
});

describe('APIレスポンスが返る', () => {
  it('HTTP200', ...);
  it('HTTP400', ...);
  it('HTTP500', ...);
});

私の使っているフレームワーク Jasmine にテストのブロックを作る describe() 関数があり、それを利用したものです。 このパターンは一番手っ取り早いです。何となくまとまりが良くなります。

なぜバッドプラクティスだったのか

「何度も検索した時」という振る舞いは「検索ボタンを押す」「検索サービスが呼ばれる」「APIレスポンスが返る」という複数のテストにまたがります。結局どこにテストを追加したらいいのか分からないままでした。

2)コンポーネント(MVC)のCをテストするパターン

function createController() {
  return new ModalController();
}

it('変数の初期状態', () => {
  const controller = createController();
  expect(controller.items.length).toBe(0);
});

it('検索サービスが呼ばれる', () => {
  const controller = createController();
  controller.startSearch();
  expect(searchService.search).toHaveBeenCalled();
});

it('APIレスポンス', () => {
  spyOn(searchService, 'search').and.returnValue([{...},{...}]);
  const controller = createController();
  controller.startSearch();
  expect(controller.items.length).toBe(2);
});

レンダリング結果のテストから解放されるため、変数の状態だけに集中できテストコードはボリュームが減って見通しが良くなります。そのため「どこにテストを追加したらいいのか」は少しだけ改善します。

なぜバッドプラクティスだったのか

実際の運用で起こりがちなバグが「データバインディング(コントローラとビューをつなぐ変数指定)のミス」「複数コンポーネント間のデータのやり取りのミス」でした。このパターンでは M,V に関するテストがないため、ビューだけ修正した際は「ノーテストです!」と付け加え、レビュアーに苦笑いでマージしてもらう事がしばしばでした。

3)レンダリング結果をテストするパターン

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

describe('初期状態', () => {
  const modal = createModal();
  expect(modal.getElementsByClassName('item').length).toBe(0);
});

describe('検索', () => {
  it('検索が正常終了', () => {
    ...
    expect(modal.getElementById('saveButton').attr('disabled')).toBe('disabled');
  });

  it('検索が異常終了', () => {
    ...
    expect(modal.getElementById('saveButton').attr('disabled')).toBeUndefined();
  });
});

describe('検索結果', () => {
  it('1件', () => {
    // リストに1件表示される
  });
  it('2件', ...);
});

ブラウザに表示される成果物とほぼ同じものをテストするため、バインディングのミスや、変数の初期化忘れによる例外などに気づきやすくなります。

なぜバッドプラクティスだったのか

画面にはたくさんのパーツがあり、ついつい欲張って「この状態の時、このボタンはこういうテキストで、こっちのボタンは非表示になっていて…」と大量にテストを書きがちです。それと DOM の構造に強く依存するため、ちょっとしたレイアウト変更であちこちテストが落ちます。

4)シナリオテストパターン

it('検索が正常終了〜保存', () => {
  // 1.検索テキスト入力
  // 2.検索ボタンクリック
  // 3.リストに10件表示される
  // 4.先頭2件を選択して保存

  // 検索リクエストの内容をテスト
  // 保存リクエストの内容をテスト
});

it('検索が異常終了〜モーダルをキャンセルボタンで閉じる', ...);
it('検索開始〜検索途中にキャンセルボタンが押される', ...);

最初に開発した時にひとつかふたつ程度シナリオを書いて、その後はバグが出るたびに追加していくパターンです。今までのパターンの中でコードは一番すっきりします。

なぜバッドプラクティスだったのか

このモーダルは「検索テキスト入力」「検索」「検索結果表示」「アイテムの選択」「保存」と、いくつかのパーツと振る舞いを持っています。API レスポンスのパターンも含めると本来テストするべきシナリオの数は相当な数になります。それとテストケース名が長い日本語になると、無駄に連なった else if 文のようで「どのパターンが抜け漏れているのか」がパッと見て分かりません。

そして全てが失敗に終わった

「仕様追加に関するコードをどこに追加したらいいのか」ってプロダクションコードで言えばオープン・クローズドの原則に関わる部分ですよね。そうか、テストコードも同じように、簡単に追加できて変更に強いコードにしなきゃいけないんだ!でも、その方法が見つからない…。相変わらず汚いテストコードを量産する日々に悩みは深まります。