テスト駆動開発をどう使うか

テスト駆動開発(TDD)についてのお勉強と考察。

「本実装の前にユニットテストを書く」というルールを自分に課して開発してみたことは何度かあるが、ユニットテストをどう書くかということも関係してそう単純なものではないことは肌で感じており、今回TDDの全体的な勉強に加えて批判的な意見についても探して、どう現実の自分の開発現場に有効活用できるか考えてみた。

私はC# WPFで自社プロダクト向けのアプリを作っているのでそういう前提。

ユニットテストでカバーする部分としない部分

私のチームでは、現時点で「コードによる自動テスト」と「エクセルに書き出した手動テスト」の2種類が行われている。自動テストでVisual Studioユニットテストの仕組みを使っており、それをユニットテストと呼んでいる。

特に規約があるわけではないが、ユニットテストはモジュール単位で動作を確認するもの、くらいの認識しかなかったので、基本的に本体のSampleクラスに対してSampleTestクラスを用意する感じで、メソッドのロジックを一通り通るようにしている。

おそらく、私のようなプログラマは少なくないだろう。クラス単位・メソッド単位で書くのではなく、状態単位で書くのがお勧めという興味深い話があったが、ユニットテストをどう書くと効果的かは改めて勉強するので今回は置いておく。

さて、私はWPFアプリを開発しているので、小さくない規模であれば基本的にMVVMパターンを使う。MVVMパターンではView、ViewModel、Modelの3層を意識して実装する。簡単に言えば、ViewはGUIGUI操作のハンドリング、ViewModelはGUIで見せるデータを持ったりビジネスロジックの呼び出しをして、Modelはビジネスロジックを表す。

トレードオフを考えた結果、私は基本的にModelにあるクラスのうちpublicなメソッドのみに対してユニットテストを書いている。ViewとViewModelには複雑なロジックは通常含まれておらず、もしユニットテストを書きたくなるようなロジックがそこにあるのであればそれは設計が適切でない可能性が高い。

また、例えば幾つかのフォルダからファイルを取得してUSBメモリにコピーする機能のようなものならユニットテストは書かない。

もちろんファイルをコピーする処理を別クラスに分けておいてそこをモックに置き換えてやればある程度の自動テストはできるが、あまり意味はない。むしろ実際にファイルをコピーする処理に上書き確認やら読み取り専用やらに関するバグが潜みやすいわけで、これはユニットテストでうまくやろうと頑張るのではなく必ず手動テストで確認すべき箇所だ。

したがって、ユニットテストを書きたくなる”匂い”を感じたら書く。

これをルールにするのは難しい。ある意味設計のようなもので、コードレビューをして不吉な匂いが残っていないか確認するのが望ましい。

テスト駆動開発をどう取り入れるか

これまで書いたユニットテストは、TDD界隈ではQA(品質)テストなどと分類されることを知った。

私のチームでは、QA担当者がブラックボックステストを行い、開発者はホワイトボックステストを行っている。これらは手動・自動に関わらず全てQAテストであり、上記ユニットテストは「粒度細かめの自動ホワイトボックステスト」である。

TDDのTはこのテストではなく、つまり粒度細かめの品質担保用の自動ホワイトボックステストによって駆動されるわけでは断じてない、ということを今回勉強して初めて知り本当に驚いた。

たぶん、ちょっとテスト駆動開発を聞きかじった程度の人は多くが誤解しているだろうし、それがTDDの普及を阻害している要因の1つだろうと思う。

TDDのエッセンスは以下だろう。(BDDは今回ほとんど勉強していないが、おそらくBDDのエッセンスでもある)

  • 他機能から呼び出す前に、よりシンプルに抜き出した形で試しに動かしてフィードバックを得ることで、あるべき姿=設計を練り、開発を前へ前へ進めていく=駆動する

このコンセプトからズレた時点でそれはTDDではない。

実際に導入する際には以下がポイントになりそうだった。

  • シンプルに抜き出すべき形というのは場合によって違い、純粋なクラス単体にするためにモックで外部結合を切る場合も、幾つかのクラスが結合したままになる場合もある
  • TDDで実装のために書くユニットテストは設計を練るためのトライ&エラーの道具であり、実装する前に設計を確定させるものではない(したがってレビューも不要)
  • テストファーストの意義はシンプルに抜き出された形でのユースケースから設計を考え始めるということ
  • ユニットテスト可能にするためには疎結合にする必要があるが、それはよく練って適切な設計にするという意味であり、盲目的に疎結合にしても保守性が下がるだけ
  • 結果的には副産物として、QAユニットテストとしては不十分だが何も無いよりはマシな程度の多くのユニットテストが残る(不要と判断した時点で消し去っても良いだろうし、将来の設計変更時にはおそらく不要になるものが多い)
  • TDDで開発するからといってアプリの全コードにTDDを適用しようとせず、ユニットテストが適さない部分では別の方法でフィードバックを得る
  • 自動化されたQAテストを行うなら、TDDの副産物として残ったユニットテストとは別に(または修正して)書いた上で、レビューするのが望ましい
  • TDDは個人のプログラミングスキルの1つであり、アジャイル開発のようなプロジェクト管理手法とはまったく別物

TDDは最終的な成果物である設計とその品質を良いものとするための1つのプラクティスであり、ルールでも目的でもない。よく言われるが銀の弾丸はなく、ケースバイケースでTDDを使うか使わないか判断が必要となる。

マイナス面としては当然ユニットテストを書く分だけ(ある意味で設計を練る分だけ)作業工数は増えるので、時間がないときにはテストで開発を駆動すべきでないこともあるだろう。これを誤解したり盲目的になるとTDDでは無くなり逆効果となる。

TDDを私の業務に活用しようとすると、やはり既に述べたようにModelの部分にのみ適用するのが効果的であるというのは変わらないと考えている。ただし具体的な実装手法についてはまだ勉強していないので何ともいえない。それについてはこの本を読みたい。

www.amazon.co.jp

既存コードのリファクタリング

既存コードの改修という場面でのTDDには特別な注意が必要だろう。

既存コードのリファクタリングでは、新規コードを書いているときのリファクタリングとは異なり、基本的には既存コードの機能を維持する必要がある。これはつまり、QAテストが必要ということを意味する。

既存コード自体をリファクタリングするときには、トライ&エラーの道具としてのTDDのユニットテストをQAユニットテストと混同してはいけない。TDDが有効なケースであっても、それ+QAテストが求められるため十分注意を払う必要がある。

どちらかというとリファクタリングの方法論に基づいて進めるべきであり、これはTDDといえばTDDだが、混同を避けるため分けて考えたほうがよさそうだ。

参考記事

www.atmarkit.co.jp

postd.cc

doda.jp

ubiteku.oinker.me

goyoki.hatenablog.com

d.hatena.ne.jp

qiita.com

qiita.com

www.geometrian.com