- トップページ
- カオスなテストコード
- 改善の第一歩
- 深い悩み
- 問題はここだ
- 先人に学ぶ(体系編)
- 先人に学ぶ(ノウハウ編)
- テストの戦略
- そして脱却へ
- おわりに
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後に再検索', ...);
});
く、苦しい…
バグが見つかったのが 半年後 というのがミソです。開発した時点のコードは半分以上忘れています。実は単純なバグでプロダクションコードはすぐに修正が完了したのに、テストコードに苦しめられる事になりました。どのような順番で何がテストされているのか、最初から最後まで読まないと何も思い出せません。
この記事で紹介しているのは擬似コードですが、実際のテストコードもこんな感じでした。半年後に自分のテストコードを読んだ私の第一の感想は「なんて汚いコード…(絶句)」でした。