🎀

テスト規模とアンチパターンについて

2022/05/28に公開

最初はテストコードを書いていたが、開発が進むに連れて書くのが大変になりやめた経験はありませんか?

その一因はテスト規模の知識が無いままにテストコードを書いていたからかもしれません。

この記事が少しでも参考になれば幸いです。

テスト規模の定義

googleでは大・中・小のテスト規模に分けられています。

小テスト

一つのプロセス(スレッド)で完結するテストです。

全体の80% を締めます。

速度と決定性[1]が高く、忠実性[2]はやや低いです。簡単に作ることが可能で保守しやすいです。

ユニットテストと呼ばれるものは大体これにあたります。

コードを1行修正したらテストを実行するぐらい高頻度なため、速度が重要視されています。
決定性を高めるために 依存性の注入(Dependency Injection) などが重要になります。

中テスト

1マシン内で完結するテストです。複数プロセス(マルチスレッド処理など)やインメモリDBと連携したテストなどが対象になります。

全体の15% を締めます。

速度と決定性はやや低いです。忠実性は小テストよりは高いです。

DBに入れるテストデータや処理順番などを意識して作る必要があるため、小テストよりもめんどくさいです。そういった理由で保守もやりづらいです。

インテグレーションテストと呼ばれるものは大体これにあたります。

大テスト

大テストは特に制限はありません。どのようなテストもできます。

全体の5% を締めます。

速度と決定性が低いです。忠実性はかなり高いです。

しかし、とても脆く指数関数的に保守コストが掛かります。

環境変数のテストやE2Eテストなどがこれにあたります。

良いパターンとアンチパターン

大・中・小のテスト規模の割合はテストピラミッドに例えられます。小テストを多く、大テストが少ないテストピラミッドにすることで高速で安定したテスト結果を提供してくれます。

スコープを限定しやすいので、認知的負荷が下がります。失敗原因の分析も迅速に行えます。

テストピラミッド

注意しなければいけないアンチパターンも存在します。アイスクリームコーン型や砂時計型と呼ばれるものです。

アイスクリームコーン型テスト

アイスクリームコーン型は殆どが手動テストか、Seleniumなどを使ったGUIテストを行います。

取りあえず動くものを作って、テストについて立ち止まって考えず、本番リリースしたプロジェクトは大抵このパターになりやすいです。

GUIテストは忠実度が高いし、画面をクリックするだけでテストを作れるのでとてもシンプルです。しかし、そのテストが遅く、脆く、不正確であることには目を向けません。

仕様が変わらない修正でもエラーが出ることが多いです。むしろ何も修正してなくてもタイミングによってはエラーになります。そのため結果が信用できないテストになってしまいます。

テストが遅いので何故エラーになったのかを確認するのも時間がかかります。

そして最終的に「テストの自動化は役に立たない」といって手動テストに戻っていくのです。

アイスクリームコーン型のアンチパターン

砂時計型テスト

ユニットテストとE2Eテストが多いパターンです。アイスクリームコーンと比べればいくらかマシですが、E2Eテストのエラーがいっぱい出るので管理が大変です。

もしインテグレーションテストがあれば解決できた問題です。

疎結合を意識して作っていないシステムがこのパターンになりやすいです。依存性[3]のない機能だけユニットテストとして書いて、それ以外をE2Eまたは手動でテストするという考えです。

砂時計型のアンチパターン

余談

すべての依存関係を結合して1画面単位でテストすることを単体テスト(ユニットテスト)と言ってる会社がたまにあります。これを単機能テストと言ったりもするらしいです。

今回説明したDBなどの依存関係をつなげない、小さいテストを単体テスト(ユニットテスト)として話すと、認識の齟齬が生まれる場合があるので注意。

「単体テスト(小テスト)をなるべくたくさん作りましょう!」
「うちの会社はほぼ全部の機能を単体テスト(単機能テスト)してるので問題ないです」

みたいな話が実際にありました。

さいごに

まずはテストの8割を占める小テストを作り、頻繁に実行するところから始めてみましょう。

最初は速度と決定性という制限がある中で実装するのは難しく感じるでしょう。でも慣れてしまえば、スケールし継続したテスト文化の大きな一歩を踏み出すことができます。
https://zenn.dev/zundaneer/articles/f07f477464d305

脚注
  1. 決定性とは依存性が低くパラメータが同じなら必ず結果が同じになること。 ↩︎

  2. 忠実性とは、どれだけ本番と同じ状態の結果を得られるかということ。 ↩︎

  3. 依存性とは、DB、メール、今日の日付、ファイル、ユーザー入力など、関数の外側の状態によって関数内の結果に影響を与える要因。結果が固定されないのでテストもやりづらい。
    例:2月の月末日を取得() == 28
    閏年のときに実行すると、このテストはエラーになる ↩︎

Discussion