単体テストのないソフトウェアまたは単体テストを書く習慣のないチームに単体テストを導入する方法

移転しました。

単体テストの習慣がない場所で必ず聞く言葉がある。

単体テストを書けば不具合はなくなるの?」

答えはNOである。

単体テストを書けば工数が減るの?」

厳密に言えば、答えはNOである。


単体テストは変更・追加開発した場合のデグレーションを減らすことはできるが、変更・追加箇所に不具合がないことを保証する方法論ではない。もちろんTDDで開発して、単体テストを予めかいておけば、いくらか不具合を減少させることはできるし、検証テストでは作るのが難しいテスト条件でのテストを行うこともできるので、まったく完全なNOではない。検証テストで発見される不具合が少なければ、不具合修正と再テストの時間を減らすことができるので、工数が減る場合もある。


でも、単体テスト銀の弾丸ではない。単体テストができるのは、テストケースで定義した状態・状況において不具合が発生すると教えることだけである。


ということを頭においた上で、テストを導入する方法を考えていかなければならない。



テストを導入するにあたって考えるべきは下記の5点。

  1. 単体テスト分の工数増加をどのようにプロジェクトマネージャやプロダクトマネージャに認めさせるか
  2. 自動化テストの環境をどうやって構築するか
  3. 自動化テストの範囲をどこまでと定めるべきか
  4. テストを書きたがらないエンジニアにどうやって書かせるか
  5. 膨大な既に書かれているテストをどうやってマネージメントするか

1. 単体テスト分の工数増加をどのようにプロジェクトマネージャやプロダクトマネージャに認めさせるか

単体テストに関係する記事や本などを読んでいると必ず出てくる問題だが、僕はこれが一番簡単な障害だと思っている。


まずひっそりと一人ではじめ、その結果が出たらマネージャに見せる。
これだけだ。


立ち上げたばかりの会社で比較すべきプロジェクトが全くない状態であればこの手法は効かないが、多分そういう会社は少ないだろう。僕の会社はプロジェクト規模大小様々あるので、必ずしも比較できるわけではないが、それでも一人でひっそりと始めることはいつからでもできる。一人で出来る範囲であれば、できることも限られている。大きいプロジェクトであれば、テストコードのない部分とある部分が必ずでき、そしてその部分での検証テスト結果が出るはずだ。
テストコードのある部分にだけきわめて検証テストの不具合発生率が低ければ成功である。
では失敗した場合は?

失敗の原因は幾つもあると思う。テストの経験がなかった、自分のスキルが低かった、テスト導入に時間をとられてコードの質が下がった(本当はそれはよくないので、そんなになるくらいなら寧ろテストは書かないほうが良い)などなどあるが、それはいくらでもあとから挽回できる。一回目ダメだったら二回目を狙おう。せっかく書いたコードはレポジトリにサブミットしておけばよい。本番では使われないコードなので多少の質は悪くても問題ない。大事なのは続けることである。


成功した場合で注意すべきは、現状の工数で問題なかったから、という理由で次回以降でも単体テスト分の工数を上乗せすることを許してもらえない場合だ。これはかなり根深い問題で、もし検証テストが思ったよりも早く終わったなら、その分を実装の工数にいれてくれというしかない場合もある。最悪の場合、検証が早く終わるならその分スケジュールも詰められるだろうと猶予分を削られる場合だ。これはもう本当に根強くこんこんと説得をし続けるほかない。上司が物分かりがよいか、新しいもの好き、もしくは周りに流されるタイプであることを願うほかない。そうでないのなら、仕方ないのでひっそりと頑張るしかないだろう。いずれにせよ、冒頭で書いたとおり単体テストは工数を短縮するための銀の弾丸ではないので、必要なときに必要なところだけを書くという運用方法に自分で切り替えていくことも必要かもしれない。

2. 自動化テストの環境をどうやって構築するか

これは自分の環境に合わせてやってください、なんだが、一応。
最低限必要なのは、

たまに仕様書・設計書を見ずに間違ったテスト対象コードからテストを書いて、不具合がつかまえられないのでテストは意味が無いという人がいるが、テストを書くときに仕様書・設計書を確認して意識的にテストNGを出して欲しいと思う。

3. 自動化テストの範囲をどこまでと定めるべきか

プロジェクトによるので一概には言えないが、結合テストシステムテストの範囲を定めるように単体テストの範囲も定めるべきである。自動化テストは(あれば)UIテストも含むので、それにはシステムテストの範囲も含まれることがある。どこからは手動のテストでマネジメントをするのかきっちり定めないと、自動化テストの範囲も決まらない。

本来は単体テスト結合テストの前までの範囲を受け持つので、結合先はモックなどをつくって擬似データを発生させる必要がある。状態の変化が必要な場合はモックがかなり高度・大規模になる場合もあり、テストが非常に煩雑になるので、そこはシステムテストの責任を委譲するのも手である。個人的にはモックを作成しなければならないテストは、結合テストの範囲とするのが一番シンプルかなと思う(が、だからと言ってしないわけには行かないこともたくさんある)

範囲として考えられるのは小さい方から順に下記の通り

  1. モックを使用しない範囲で正常系だけを通す(プロジェクトが小さい・不具合発生時のリスクが小さい場合)
  2. モックを使用しない範囲でデシジョンカバレッジを100%にする
  3. モックを使用しない範囲でコンディションカバレッジを100%にする
  4. モックを使用して正常系を通す(結合時の不具合発生が予想され、かつ不具合の特定が難しいと判断出来る場合もしくは結合時の不具合発生をできるだけ抑えたい場合)
  5. モックを使用してデシジョンカバレッジを100%にする(状態の変化をデータで持っておく必要があるため、テストデータが爆発的に増える)
  6. モックを使用してコンディションカバレッジを100%にする
  7. モックを使用したテストが出来るように全体的にコードの設計を見直し、ステートメントカバレッジを100%にする(非現実的)

個人的には2までにするか、4までにするか5までにするか、それ以上を頑張るかの判断を最初ですべきだと思う。
僕の扱うプロジェクトはソフトウェアだけで完結するものではなく、自分では手を加えることのできない他社製品を制御するのがほぼ必須である。この場合は不具合発生時のリスクを見積もった後、2か4かの選択をする。大抵の場合モックが必要な箇所は4, 必要ない場所は3で作る事が多いかな。枯れたシステムを制御する場合は2でもほぼ問題がない。制御するシステムが増えてくると4または5でないと、問題切り分けができなくなる。
また、くわえて上位の自分では手を加えられない大規模なシステムと結合しなければならない場合もある。この場合は結合テスト時の不具合発生の問題切り分けが非常に煩雑でリスクが高いので、4または5を目標にすることが多い。余力がある場合は6。一部システムテストで責務を負うべきところも単体テスト・自動化テストでやってしまうこともある。ここらへんは品質みつつという感じかなぁ…

4. テストを書きたがらないエンジニアにどうやって書かせるか

テストを書くのは意味が無いという人は必ずいるし、その信念はひとつの真理ではある。社会人として尊重した上で、それでも書いて欲しいとお願いするときにどうするかということを考えていかなければならないこともある。社会人なので。

テストを導入する手順は次のようにするとわりとスムーズだ。

  1. まず一人である程度枠組みを作る(レポジトリへのサブミットと同時に単体テスト走らせ、その結果を通知または閲覧できるようにする)
  2. コードレビュー時にかならず単体テストコードもレビュー対象に含める
  3. 新しいもの好きな人を巻き込み、複数人でプロジェクトにテストを含める作業をする
  4. いろいろ問題が出てくると思うので、テストコードを改善する
  5. テストをかきたがらないエンジニアのコードレビュー時にテストは?と必ず聞く
  6. 検証テストの不具合発生時、不具合発生理由を聞いた際にまずテストを書いてそれが本当かどうかエビデンスを出してくれと頼み、テストの書き方を教える。書けるまで徹底的に付き合う。その場合の文句とかはちゃんときき、一緒に解消方法を考える
  7. 必要ならテストをフレームワーク化する
  8. 不要な部分までテストが必要!という人がいたら、こういう目的で今回は単体テストを行っているので、あればよいですが無理しなくてよいですと伝える
  9. プロジェクトの変更・追加の前に単体テストの責務の範囲についてチームで話し合う。また検証テストと単体テスト・自動化テストの被る部分についてコンセンサスをとっておく
  10. 2, 4, 5, 6, 8, 9を繰り返す

スキルのないエンジニア(俺のことだ)の場合要求分析の練習にもなるので、6とかはすごく大事だとおもう。9ができるようになればほぼ定着していると思うので、そのまま続ける。新しいもの好きな人はあきるのも早い場合が多いので、ある程度テストコードが増えてきたら、テストを書きたがらないエンジニアを巻き込むことを考え始めたほうが良い。

5. 膨大な既に書かれているテストをどうやってマネジメントするか

TDDなどでは殆ど触れられないが、テストコードのマネジメントは非常に難しい。プロジェクトの規模が増えればもちろんテストコードは増えるし、結合先の状態が変わるテストをしようと思ったら、テストデータも増える。

テストコードにはコメントを書く

そもそもテストコードは読みにくい。僕はコメントはできるだけ書かない派(どうしても必要な箇所にだけ書くべきだが、それ以外は書くべきではない派)だが、テストコードにはコメントを入れておいたほうが良いかもしれない。テストケース名も、どれだけ長くなっても見るだけで何のテストかわかるほうが良い。テスト失敗したっていうメールが飛んできたけど何のテストでコケたのかパッと見分からないというのは結構不幸だ。

また、テストコードはできるだけテンプレート化しておいたほうが良い。これは複数人で同じテストコードを書く場合・スキルの低いエンジニアがテストを書く場合もあるためだ。むしろテストコードはスキルの低いエンジニアこそ書くべきなので(その人が書く場所は不具合が発生しやすくなるため)、テンプレートとガイドラインは必須だと思っておいたほうが良いのかもしれない。


テストコードをオブジェクト指向にする

状態を持つようにしたり、特定のテスト条件のデータを返す必要がある場合、データはデータ用コンポーネントに全て持っておいたほうが良い(DBのことではない)。またテスト条件を作成するコンポーネント、テストを実行するコンポーネント、モック作成のコンポーネントなど、オブジェクト指向にすればするほど、複数人での並行開発が楽になる。もちろん単体テストの範囲が狭い場合はそこまで頑張る必要はない。

寧ろテストは頑張らないくらいの勢いで書いていないと続かない

テストコードをレガシー化させない

テストが膨大になるに連れ、必ずレガシー化したテストがでてくる。だが単体テストの場合、そのテストを飛ばして実行するということができてしまう。これではテストの意味がない。
もしレガシー化している・なんのためにあるのかよくわからないテストが出てきたら、そのテストを実行し、なにをテストしているのかを確かめたあと、書き換えよう。時には捨ててしまうこともありかもしれない。
テストコードもソースコードの一部なのだ。テストコードだからといってレガシー化させてよいわけではない。死んだコードは不要なコードだ。おなじステートメントを通るテストケースが2つあり、そのふたつが別である意味がないのであれば、片方は削除すべきだ。別である意味があるのなら、それはコメントに書くべきである(大抵の場合は意味ないが)。

テストコードはできるかぎり低コストで高パフォーマンスを実現したほうがよい。必ずしも上司やチーム内に認められるわけではないテストコードだからこそ、それは常に頭の片隅において置かなければならない。しかし開発時のどこからともなく来る不安や、不具合発生時のやけくそなテストコードは別段非難されるべきものではない。あとから適切に処理してやればよいのである。そんな時間はないだろうか? だが、処理をするのは、テストコードを書いた人でなくてもいい。時間がないなら、作る。作れないのなら、他の人に委譲する。一人で開発しているのでなければ、たぶんきっとできるはずだ。


http://www.hyuki.com/yukiwiki/wiki.cgi?FlawedTheoryBehindUnitTesting