テスタブルコードの書き方 - 基本戦略編

今のチームにテストコードの導入を本格的にしようと思ってるので、思考の整理がてらメモ。内容は初学者向け。

テストの必要性をとくのは比較的簡単である程度できた。既存のレガシーコードはとりあえず忘れることに(特定メンバーでプロジェクト的に実施)。

というわけで、新規コードはみんなテスト書いてね! 

と、これだけでテストを書いてくれるでしょうか? 答えは否でした。

原因として自分の書きたいコードをどうテストすれば良いかわからないというものです。

新規コードなのでテストをしやすいように設計をすれば良いだけです。TDDはそれを支援してくれる有効な手法です。 しかし、テストが無い環境に慣れた人間はそもそもテスタブルコードを見慣れてません。なので、自分の書きたい実装をどう書けばテストが書きやすくなるかが分からないのでテストコードが非常に複雑になったり、立ち止まったりしてしまいます。

なので、テスタブルコードの基本戦略と事例集をまとめてみました。

基本戦略

  • 戻り値が無いコードを書かない
  • 同じ引数では同じ結果を返す
  • I/Oはそれに専念する
  • 依存性は注入できるようにする

戻り値が無いコードを書かない

Javaで言えばvoid型のメソッドです。なんというかテストを書くことを意識してないコードの典型例です。

戻り値が無いコードは後述するI/Oを含んだコードである可能性が高く、I/Oのテストはコストが高いものです。 なので原則的には単純に値を返すシンプルな関数を実装するのがコツです。

同じ引数では同じ結果を返す

例えば日付、例えば乱数。こういった実行される度に結果が変わる値って存在しますね?

こういった値をロジックの内部で使っていると同じ引数でも毎回違う結果になってしまいテストを記述出来ません。 なのでこの手の値は関数の引数にとったり、後述の依存性の注入を使うことでロジックから切り離してしまいましょう。 こうすることで、ロジック自体は簡潔にテストを書くことが可能になります。

I/Oはそれに専念する

ファイルの入出力、DBアクセス、ネットワークアクセス、標準入出力、ログ、画面、こういったI/Oはテストの敵です!

これらをテストすることは不可能ではありませんが、記述量的にも実行速度的にもコストが高いテストになりがちです。また、DBやネットワークの場合はテスト向けの環境を用意するのも頭を悩ませる問題です。 こういった部分はなるべく減らして、場合によってはテストコードではなく別の方法で実施しましょう。そのためにはI/Oとロジックを分離してロジックの部分だけをUTできるように分離するのが重要です。

依存性は注入できるようにしよう

依存性ってなに? とか言われそうですが、ようは前述してるI/Oや日付、乱数なんかを取り扱うオブジェクトをクラス内部でnew(初期化)せずにコンストラクタやsetterで外から設定できるようにしよう、という考え方です。

DIコンテナとか使うと、こういった設計でプロダクトコードを書きやすくなりますが、必須ではないです。 逆にDIコンテナ使ってても、外から設定できるようにすることを意識してないと、とても面倒なコードになりがちです。

フィールド値はすべてコンストラクタやsetterで設定できるようにするか、package属性(default)で記載するとテストコードを非常にシンプルに書けます。

まとめ

とりあえずこの辺が基本的に意識することになると思います。 とはいえ、これだけで具体的にどう書くか分かる人はすでに書き方が分かってる人だと思うので、明日は具体的な事例集を書きます。

それではHappy Hacking!

参考