ふつうのユニットテストのための7つのルール

公開:2020/09/28
更新:2020/09/28
7 min読了の目安(約4300字TECH技術記事

はじめに

「テストが大事」これは生産性と品質を高めるための基本的な考え方です。なので「テスト書かないと」ってことはみんな同意するのでUnitTestらしきものが書かれてることは多いのですが、役に立たなかったりむしろ有害ですらあるテストがあふれているプロジェクトもあります。

それはユニットテストを有効活用するための最低限のルールが守られてないときです。あまり学校で習う類のものでもないかもしれないですし、大事だと思うので個人的な見解をまとめておきました。

Javaをベースに書いていますが書かれている内容自体は言語には依存しないかと思います。

前提

ここでは「ユニットテスト」をJUnitのような関数レベルの自動テストを想定しています。
また、Mavenを使ってローカルビルド時に常に走らせるような状態を想定しているので、そこを踏まえたルールだと思ってください。

ルール

1. ポータビリティを大事にする

CIサーバや他人のPCで動かないテストコードを書く必要があります。他所で動かないコードは不要では無く害悪です。
具体的には後述のルールでも具体的に触れていきますが、まずこの大前提を忘れないでください。

例えば 「Gitリポジトリ配下以外のディレクトリのみを使用する」 というのは最も気にしないといけないポイントです。例えば 本番環境の「/var/hogedata」や「D:\hogedata」みたいなパスを前提にしたテストを書かない、ということです。mavenの場合は一時ファイルは target ディレクトリのみに出力したり、リポジトリ内を相対パスで指定します。別途記事にしますが本番コード自体がそういった出力先などのハードコーディングを避けることが重要になります。

また、DBなどが必要な場合は環境設定の差異を減らすためにDBUnitTestContainersを使うと良いと思います。

2. 繰り返し実行できるように作る

ポータビリティと同じくルールの根幹です。
手作業なく繰り返し実行できることはUnitTestでは必須の要件となります。

具体的には環境の初期化と後始末を含めてすべてをテスト内で表現してください。setUpとtearDownで初期化とクリーンアップを適切に実施することで解決できます。

特に、日付や乱数の取り扱い、DBやファイル出力、またはAPIで外部にリクエストを投げる際は注意をしましょう。

3. Unit Testですべての自動テストをしない

Unit Testは軽量で高速ななFast Test集合であるべきです。

UTは唯一無二の自動テストではありません。UIの挙動やDBやシステム間連携といった実行時間のかかるSlowTestはCI上の結合テスト/機能テストで行ったり、SeleniumなどのE2Eテストなどに任せてUTはなるべく軽量で外部依存のない形で実装するべきです。

これは前提に書いた通り 「ビルド毎にUTを実行する」 という前提を守るためです。ビルドの度にDBの初期化など重い処理をやってると生産性が悪すぎるのでついつい-Dmaven.test.skip=trueのようなテスト無効化オプションの利用に繋がりテストを腐らせる原因となります。

そのため、UTの文脈ではモックなどを利用してビルド毎に回せるテストに限定して、ビジネスロジックのうち実際のDBを取り扱うテストは結合テスト/機能テストの自動化など別のフェーズでやるべきです。
「例え自動テストの実装がすべてJUnitだとしてもテストの階層化を意識する」 という事を忘れないでください。言い換えれば 「なにをUTで担保しないか」 を明確に意識しましょう。

4. モックやスタブを過度に使わない

ファイル/DBやネットワーク(WebAPIとか)といった重たかったり副作用を伴う処理はモックやスタブといったTest Doubleを使う事で依存性を切り離してFast Testとして実行出来るようになります。
例えばBetamaxJMockitを使う事で挙動のエミュレーションも簡単に出来ます。

これは非常に便利なのですが 使い過ぎないように注意 しましょう。
なぜならテストダブルを多用すると テストダブル側にビジネスロジックが入り込む 事が良くあるからです。典型的なのはDBで複雑なSQLを伴うビジネスロジックの検証の場合はDBのモックに同様のビジネスロジックを実装して** 生産性の低下だけではなくテストの意味が曖昧になる** ということも起こります。

SQLのようなデータ取り出しの部分とビジネスロジックを分ける事が重要ですし、SQL自体がビジネスロジックと不可分なケースは無理にモックを作らず、DockerやTestContainersを使って 本物のDBを使う事をお勧め します。
その場合はSlowTestになってしまうので、UTではなく別のフェーズで実行するテストにする という点も意識しましょう。

5. ひとつのテストケースにはひとつの観点しか検証しない

一つのテストケースでは一つの観点のみを検証します。つまり原則1テストケース1アサーションです。

これは複数のアサーションが混じっているとテストが失敗ときに原因の切り分けが出来なくなるからです。手動で実行してる分けではないので 同じような処理を実行するからとテストケースの圧縮を過度にする必要はありません。機械は同じ作業を何度でもしてくれるでしょう。

もちろん、同じ観点でのパラメータ化テストや一つの検証観点で複数の検証項目がある(例えばBeanの各プロパティをチェックとか)場合にassertをいくつも書くのはまったく問題ありません。

問題なのはテストXを実行したときに「ロジックAの動作」と「ロジックBの動作」を同時に検証することです。たとえ、AとBが依存してても個別のテストケースに落とすべきです。

6. テスト内容が分かる名前をつける

名前は大事。

稀にユニットテストの中が「test001」から「test010」まで並んでいて、なにを検証してるのかが別で作られてる Excelのテスト仕様書をみないとサッパリわからないUT があります。
テストケースはふるまいを確認するための重要なコードなので、きちんと名称等にも気を使って書くべきです。

とくに日本語圏のエンジニアしか居ないなら思い切ってテストケースを日本語にするのも良い解決策です。Javaなど最近の言語は日本語でメソッド名をかけます。私も含めて多くの日本人は英語より日本語の方が得意なので非常に書きやすくそして読みやすくなります。プロダクトコードに日本語変数や日本語メソッドを使う時よりデメリットも少ないです。

@Test
public void 翌日の日付を求める() throws Exception {
@Test
public void nullを渡すとNullpointExceptionが発生する() throws Exception {

もちろん、英語で書いた方がグローバルで活用しやすいとかもあるので、そこは選択肢ですがその場合はコメントやその他仕様書も同様なので天秤にはかけてみてもいいと思います。もちろん英語で分かりやすい名前を付けても全く問題ないです。
コメントでも良いのですが多くとテストツールがレポートを作成するときにテストケース名を使うので、英語でも日本語で良いのでわかりやすいテスト名がより重要 です。

BDDのテストツールを使うとより書きやすくなるかもしれませんが、それは別の話題になるので割愛。

7. ログや標準出力は極力出さない

たまにUTで詳細なログを出すケースがあります。これは原則避けるべきです。
なぜテストにログや標準出力を出すのでしょうか? それは人間が詳細の動きを確認するためです。つまり、雄弁なログを出すUTは 「テスト結果の目視チェックの疑い」 があります。

テストの結果で判断したいものが単純に関数の戻り値ではない場合、その結果をログに吐いて目視で見ているかもしれません。実行時間やファイルなど外部出力が典型的なケースです。こういった情報もきちんとUT内で完結する必要があります。すべての検証項目をUT内のアサーションでチェックしていればそもそもログは一切不要 のはずです。

もちろん、テスト不具合時にログの詳細を出したいとかはあると思うのでそれは適切なログレベルを選べば良いのですが常日頃から出すものではありません。特にデバッグモードだとcatchしたエラーのスタックトレースを標準出力に出す、見たいになってるコードだと本来のエラーのスタックトレースと混在して可読性が最悪になります。

まとめ

この記事は以前書いたものをベースにしているのですが、4年前ですし結構改稿しました。

今回書いたものは何か有名な本に載ってるとか学術論文を元にしてるわけではないですが、個人的な経験としては「最低限というかこれやってないと破綻するな」というレベルです。そして破綻も何度も見てきましたし、知らなきゃわからないとも思います。私もここにあるアンチパターンを自分自身も書いて学んだことも多いです。

「JUnitの使い方」のようなHow toの先、こういう部分こそが大事だなと最近よく思うので、改めて明文化してみました。

実践にあたっては名規則とかテストの種別によるJUnitや各テストツールの具体的な使い方をまとめた資料が必要になってる来ると思いますが、テクニカルなところは結構世の中にも良い資料がたくさんある気がするのでそっちにお任せ。

それではHappy Hacking!