概要
「ユニットテスト」という言葉や手法について,聞くことは多いと思います.また,チームで「テストを導入しよう」という話をだったり,究極的には「手動テストが辛いから自動テストにしたい」という要求も,よくあると思います.しかし,実際のところ,「ユニットテスト」が,「何の目的」があり,「どのような利益」があり,「いつ利益が享受できるか」と言ったことは,あまり焦点に上がらず,「とりあえずやろう」という曖昧な状態で始めてしまうことが多い気がします.そのため,この文章では,「ユニットテスト」について,包括的に整理し直し,「ユニットテストの目的・定義・価値」の説明を試みました.
ユニットテストの目的
ユニットテストの目的とは
ソフトウェアの保守性を保つこと
だと私は考えています.ここで"保守性"と呼ばれる性質は何か.というと,ソフトウェアの品質に関する国際基準であるISO/IEC9126における"保守性"です.ISO/IEC9126は以下のようなソフトウェアの品質を定義しています.
品質特性 | 詳細 |
---|---|
機能性 | ソフトウェアが使用される目的に合っていること |
信頼性 | ソフトウェアが安心して使える度合い |
使用性 | ソフトウェアの使いやすさ |
効率性 | ソフトウェアの性能 |
保守性 | ソフトウェアが仕様変更に強く,テストが容易なこと |
移植性 | ソフトウェアが現在動いている環境から,別の環境への移行が容易なこと |
したがって,言い換えると,
「ユニットテストは,ソフトウェアが仕様変更に強く,テストが容易な状態を保つために行うこと」
ということになります.
ユニットテストの定義,そのほかのテストとの違い
世の中にはいろいろなテストがあり,そのテストの境界は人や企業によって異なっていたりして,非常に曖昧です.その中で一番,自分が曖昧性が少ない定義は,「実践テスト駆動開発」におけるテストの定義だったので,それを紹介します.
分類 | 説明 |
---|---|
受け入れテスト | システム全体が機能するか? |
インテグレーションテスト | 私たちが変更できないコードに対して,書いたコードが機能するか? |
ユニットテスト | オブジェクトは正しく振舞っているか?また,オブジェクトは扱いやすいか? |
特にインテグレーションテストとユニットテストの境界が秀逸であり,「インテグレーションテストは,私たちが変更できないコードに対して,書いたコードが機能するか?」ということが明言されています.実例として想像しやすいのが,データを保存する機能を持つクラスだと思います.いわゆるRepository Patternを用いたクラスであったり,ActiveRecordのようなDBと密結合しているようなクラスにあたります.このようなクラスはユニットテストはできず,インテグレーションテストを行うほうがベターです.逆にユニットテストは「私たちが変更できるコードに対して,書いたコードが機能するか?」をテストすることになります.
角の立つ言い方かもしれませんが,
関数の引数にフレームワーク固有の型を使った時点で一切ユニットテストができないと思ったほうが良いです.また,そのようなフレームワーク固有の型を使った関数を持ったクラスも,一切ユニットテストができないと思ったほうが良いです.
RailsであればActiveRecordを用いたクラス.他では,ORMを用いてDBにデータを永続化する際に,ライブラリ固有の型を継承したクラスを実装し,データ操作を行う場合も多いと思います.私見ですが,このようなクラスは大体ユニットテストは書けないです.
現在,モックのライブラリが高機能化してきており,いわゆるライブラリのクラスも無理やりモック化できるようになっています.したがって厳密にはテストができます.しかし,それをやってしまうと"テストコードの保守性"が下がり,"テストコードの保守工数"があがることになります.私も何度か地獄を見ました.
ユニットテストの価値
ユニットテストは"保守性を保つこと"と最初に述べました.それによってどのような利益が生まれるのか?という疑問が生まれます.それは,
ソフトウェアの機能の継続的な拡張
です.これは,最近ではMartin FowlerのIs High Quality Software Worth the Cost?という記事で議論されており.以下のようなグラフが掲載されています.
横軸は時刻で,縦軸は機能数を表しています."high internal quality"なソフトウェアは時間経過とともに,機能数が線形に近く上昇していますが,"low internal quality"なソフトウェアはある程度の時間が経つと,機能数が鈍化していることが分かります.
当該記事では,"internal quality"は"ソフトウェアの内部の質"を定義されています.これはこの文脈ではほぼ「保守性」と同義だと思って問題はありません."高い内部の質"を保つことで,ソフトウェアを長期的に成長させることができます.しかし,図の通り,内部の質を求めないソフトウェア開発のほうが,短期的には機能の開発速度が速くなる.というジレンマがあります.
では,低い品質の開発と高い品質の開発は,いつ開発速度が逆転するのか?という疑問が生まれます.これは,図にも書かれていますが,"this point occurs in weeks(not months)"と書かれています.つまり,高い内部の質を保つ方が1ヵ月以内に開発速度が加速すると筆者は書いています.ただし,これは疑似グラフであるため,定量的な議論は難しい.とも書かれています.
どれだけユニットテストを書くべきか
現在,ユニットテストにのみ焦点を当てて議論していますが,その他のテスト.「受け入れテスト」や「インテグレーションテスト」と,どれくらいのバランスで書けばよいか?という話があると思います.まとめて書くと
ユニットテストが70%
インテグレーションテストが20%
受け入れテストが10%
です.これはGoogleTestingブログのJust Say No to More End-to-End Testsという記事に書かれています.なぜユニットテストが重要か?それは「適切なフィードバックループの構築(Building the Right Feedback Loop)」という目的があります.基本的に,バグを修正するときには,
- バグを再現するテストを書く
- 機能を修正する
- テストを動かし,正常に動作するかを確認する
というプロセスになります.そのプロセスを円滑に動かすためには,
- テストが速く動くこと(It's fast.)
- テストが安定して動くこと(It's reliable.)
- テストの障害を分離できること(It isolates failures.)
の3つの性質を満たすことが重要になります.
1.は言わずもがなテストが高速に動作することで,早く実行結果が確認できるため,デバッグが高速化します.2.は安定して動くことで,テストを動かすための不確実性を減らし,デバッグに関する工数を減らす効果があります.例えば,Rest APIを呼び出すようなテストであれば,呼び出し先のサーバーが止まっていたり,一時的な障害が起こっている可能性があります.そのようなテストは避けたほうが良い.ということです.これが2.です.また,e2eテストでseleniumでブラウザを動かして,ページを表示することがあると思います.そのような場合,障害点が複数あり,selenium driverが動かなかったり,ブラウザが動かなかったり,サーバーが応答しなかったりします.e2eテストで失敗した場合,そのような障害点を1つ1つ確認していく作業が必要になります.そういうことは避けましょう.ということです.これが3.です.
これらの3つの性質を満たすのがユニットテストであるため,ユニットテストを多く書きましょう.という理屈になっています.
個人的にはテストには慣れが必要だと思います.そのため,最初はユニットテストだけひたすら書くのが良いと思います.
テストの経済性
「関数の引数にライブラリ固有の型を使った時点で一切ユニットテストをができないと思ったほうが良いです.また,そのようなライブラリ固有の型を使った関数を持ったクラスも,一切ユニットテストができないと思ったほうが良いです.」
と「ユニットテストの定義,そのほかのテストとの違い」で書きました.その理由はなぜか?というと,テストのアンチパターンを引くと,逆に工数が膨らむからです.
これは,xUnit Test Patternsから学ぶユニットテストの6つの目指すべきゴールに"テストの経済性"という節に説明があります.この内容のもとはxUnit Test Patternsという割と初期のテストに関する書籍が出展のようです.
自動化テストを構築・メンテナンスするのには、それ自身にコストがかかります。
しかし、自動化テストの構築・メンテナンスにかかる追加コストは、テストによる節約コストによって 相殺 されます。テストによる節約コストとは、 手動によるユニットテストの回避 や デバッグ/トラブルシューティングの削減 ・ 正式なテストフェーズ・本番稼働の初期まで検出されなかった欠陥の修正コスト です。
このように"テストを適切に構築できる"と,テストによる工数節約が効率的に機能し,総じて開発コストが下がっていきます.しかし,テストのアンチパターンを行ってしまうと,
しかし、テストの読み書きが難しかったり頻度高くコストの掛かるメンテナンスを要すような状況では、次の図の示すようにソフトウェア開発におけるトータルのコストは上がっていきます。
という風に,悪いテストコードを書いてしまうと,テストコードを保守するのに工数が嵩んでしまい,テストによる工数節約を享受できず,開発速度が遅くなってしまいます.そのため先に挙げた「ユニットテストに外部のライブラリを含めない」ということを心がけておくと,これらの障害をかなり回避できる印象があります.
ユニットテストのまとめ
- ユニットテストを重点的に書きましょう
- ソフトウェアを高品質な状態を保てば1ヵ月以内に開発速度が上がる
- 間違ったユニットテストを書くと,開発速度が鈍化するので,適切なテストを書きましょう
感想
最近,テストに関する資料を読み漁っていた結果,テストを統一的に議論するのは,かなり難しいことが分かりました.その一端が,テストとは何か.いろいろなテストの定義と分類.という記事でした.その一方で,テストについて質問を受け,では,究極的にテストについて一番コアな部分を議論するとどこか?となると「ユニットテスト」というところに行きつきました.これ以上の外部の話を盛り込むと,焦点がぼけてしまい,「みんながみんなのテストの定義を持ってる」から入らないと難しくなってしまうため,一度ソリッドに全部切り落としてまとめてみました.
テストをまじめにやってる人には割と怒られそうな文章かもしれませんが,何かの役に立てば幸いです.