01.カオスなテストコード

下記のコードを見てください。私が1年ほど前に書いたテストコードのスタイルです。

※ 説明のための擬似コードで、テスティングフレームワークは Jasmine を使っています。

describe('アイテム検索・登録モーダル画面', () => {
  beforeEach(() => {
    spyOn(searchService, ['search']);
  });

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

  it('初期表示', () => {
    const modal = createModal();
    expect(modal.getElementById('searchText').value).toBeNull();
    expect(modal.getElementById('searchButton').innerHTML).toBe('検索開始');
    expect(modal.getElementsByClassName('item').length).toBe(0);
  });

  describe('アイテム検索', () => {
    it('検索結果がある', () => {
      const modal = createModal();
      modal.getElementById('searchText').value = 'カルテットコミュニケーションズ';
      modal.getElementById('searchButton').click();

      searchService.search.and.returnValue(Promise.resolve([
        { id: 1, name: 'item1' },
        { id: 1, name: 'item2' },
      ]));

      expect(modal.getElementsByClassName('item').length).toBe(2);
    });

    it('検索結果がない', () => {
      ...
    });
  });

  describe('検索結果からユーザーが任意のものを選択できる', () => {
    it('1件選択', () => {
      const modal = createModal();
      modal.getElementById('searchText').value = 'カルテットコミュニケーションズ';
      modal.getElementById('searchButton').click();

      modal.getElementsByClassName('item')[0].click();

      searchService.search.and.returnValue(Promise.resolve([
        { id: 1, name: 'item1' },
        { id: 1, name: 'item2' },
      ]));

      expect(searchService.search).toHaveBeenCalled();
      expect(modal.getElementsByClassName('selected-item').length).toBe(1);
    });

    it('10件選択', () => {
      ...
    });
  });

  describe('検索結果がHTTP400だったら何も選択できない', () => {
    it('選択のためのリストは空の状態になる', ...);

    it('登録ボタンはdisabledになる', ...);
  });

  describe('検索結果がHTTP400でも再検索はできる', () => {
    it('再検索の結果がHTTP200の場合', ...);

    it('再検索の結果がHTTP400の場合', ...);
  });

  it('APIリクエストのアクセス権がないユーザーが検索した時', ...);
  
  describe('検索中は検索ボタンはdisabledになる', () => {
    it('検索前の状態', ...);
    it('検索中の状態', ...);
    it('検索後の状態', ...);
  });

  // 以下、続く..........
  // どんどん続く..........
});

読めばなんとなく分かると思いますが、検索ボタンと結果表示のリストが配置されたモーダル画面をテストしているものです。

ちょっとした仕様変更

書いている時はなにも問題ないんです、上記のコード。何が苦しいかって仕様変更の時。 私の働いているカルテットコミュニケーションズ開発部は、上から絶対命令の仕様が降りてくるV字モデルの開発スタイルではなく、エンジニアからのちょっとした提案もウェルカムな体制です。「この部分は使いづらいんじゃないか」「エラーメッセージを表示したらユーザーに分かりやすいかも」などなど、小さな提案と変更はしょっちゅう繰り返されます。

そこで「検索ボタンを押した時に “検索中” と分かるようにクルクル回るアイコンを表示しよう」という新たな仕様が追加されました。

さあテストを追加しよう

上記のテストコード、実際のものは 1000 行を超えています。正常パターンの API レスポンスに加え、異常パターン、ボタンの disabled などレンダリング結果のチェックや、ボタンを押した時にサービスがコールされる事など「モーダル画面のありとあらゆる振る舞い」を網羅しているためです。(振る舞いを持ちすぎたコンポーネントはリファクタして分割していくべきですが、今回はテストのお話なのでそこは目をつぶってください…)

はてさて、どこにテストコードを追加するのが正解でしょうか?

it('初期表示', ...);

describe('アイテム検索', () => {
  ...
});

describe('検索結果からユーザーが任意のものを選択できる', () => {
  ...
});

describe('検索結果がHTTP400だったら何も選択できない', () => {
  ...
});

describe('検索結果がHTTP400でも再検索はできる', () => {
  ...
});

it('APIリクエストのアクセス権がないユーザーが検索した時', ...);

describe('検索中は検索ボタンはdisabledになる', () => {
  ...
});

// 新しく追加された仕様だし、ここか!!😃😃😃
it('検索中はアイコンを表示しユーザーに分かるようにする', () => {
  ...
});

新規のテストを追加し、無事に仕様変更がリリースされ、めでたしめでたしです。

ところが 半年後に「API レスポンスが HTTP 500 の時にアイコンがクルクルしたまま、しかもなぜか 1 回目の検索では正常で 2 回目の再検索の時だけ」というバグが発見されました。はてさて、どこにテストコードを追加するのが正解でしょうか?

it('初期表示', ...);

describe('アイテム検索', () => {
  ...
});

describe('検索結果からユーザーが任意のものを選択できる', () => {
  ...
});

describe('検索結果がHTTP400だったら何も選択できない', () => {
  ...
});

describe('検索結果がHTTP400でも再検索はできる', () => {
  it('再検索の結果がHTTP200の場合', ...);
  it('再検索の結果がHTTP400の場合', ...);
  it('再検索の結果がHTTP500の場合', ...); // 候補1)ここか...?🤔🤔🤔
});

it('APIリクエストのアクセス権がないユーザーが検索した時', ...);

describe('検索中は検索ボタンはdisabledになる', () => {
  ...
});

it('検索中はアイコンを表示しユーザーに分かるようにする', ...);

it('検索結果がHTTP500だった時', () => { // 候補2)ここかなあ...?🤔🤔🤔
  ...
});

// 候補3)検索まわりのテストをまるっと書き直すか?🤔🤔🤔
describe('検索の挙動', () => {
  it('HTTP200', ...);
  it('HTTP400', ...);
  it('HTTP500', ...);
  it('HTTP200後に再検索', ...);
  it('HTTP400後に再検索', ...);
  it('HTTP500後に再検索', ...);
});

く、苦しい…

バグが見つかったのが 半年後 というのがミソです。開発した時点のコードは半分以上忘れています。実は単純なバグでプロダクションコードはすぐに修正が完了したのに、テストコードに苦しめられる事になりました。どのような順番で何がテストされているのか、最初から最後まで読まないと何も思い出せません。

この記事で紹介しているのは擬似コードですが、実際のテストコードもこんな感じでした。半年後に自分のテストコードを読んだ私の第一の感想は「なんて汚いコード…(絶句)」でした。