読者です 読者をやめる 読者になる 読者になる

独習単体テスト

私は、テストで重要なのはカバレッジでなく、
「こうあるべき」と定義したAssertionであると思っています。
さらに言うと、データ/状態/操作の組み合わせパターンを変えて、
さまざまなAssertionのパターンを定義できる*1人が
テストのスキルを持っていると言えるのではないでしょうか。


JUnitを利用して単体テストを行う時に「実装したソースコードを元にテストクラスを記述すること」は
非常に時間がかかってしまうものです。
実装によっては、そのメソッドが「何をすることを期待しているのか」
定義することが複雑になる為です。
大半の現場では、Assertionが定義しづらく、カバレッジ率のみで
「テストを実施している気になっている」ことが多いのが大半でしょう。
テストしてる感も実感でき、カバレッジ率を出すだけにならないテストクラスにする為には、
実装する前に意識することがいくつかあります。

処理の分割

大きな処理のテストは、「何がどうなれば正しい振る舞いなのか」定義しづらいものです。
計算してDBに格納する処理だとしても、

  • 計算する
  • DBに格納する

という2つの振る舞いを確認しなければなりません。
計算も、明細の計算、合計の計算等様々ある場合、パターンを変えて、
何回も大きな処理を呼び出し、結果を確認していませんか?
結果の確認方法も、戻り値に設定されないのであれば、
DBに格納された値を確認する等の間接的な方法しかないはずです。
DBへの格納処理が間違っていた場合、計算自体が間違っていたのか、
格納の際に余計な処理を行っていたのか、見分けがつかず、
結局両方の処理をデバッグで追うことになりかねません。
せっかくテストクラスを作成しているのに、です。


そこで、処理を分割してみることをお勧めします。

  1. 大雑把に処理の流れをイメージ
  2. 呼び出せば、「1」の処理が実現できるインターフェースメソッドを定義
  3. 「2」のJavaDocに大雑把な処理のイメージを記述。
  4. 「2」のメソッド実装は、別のメソッドを呼び出すだけに徹する
  5. 「4」で定義したメソッドの処理内容を見て、別Layerかクラス内メソッドにするか判断する*2


クラス内メソッド処理の単位は
中途半端に処理をして、中途半端に処理を他メソッドに委譲するのは止めて、

  • 呼び出したら処理をして結果を返すだけにする

もしくは

  • 他のメソッドを呼び出すコントロールに徹する

のどちらかにした方がよいでしょう。
そして、重点的にテストをするのは、処理をして結果を返すだけにするメソッドです。
メソッドの責務がはっきりしている為、テストクラスも作りやすくなっているはずです。
さらに、全て引数で処理が行えるようにすると、テストクラスも記述しやすくなります。
Utilクラスのテストクラスが書きやすいのは、全て引数で完結しているからです。
ビジネスロジックの処理とはいえ、小さな処理の固まりにしてしまえば、
処理自体は単純なものになるはずです。
また、メンバ変数は、DIコンテナからInjectionするオブジェクトくらいにしておき、
極力状態を持たないようにすることで、Threadセーフな実装になります。*3

Mock≠スタブ

Mockをスタブのように、決められた値を返す為だけに使うのはもったいないことです。
「(スタブとして使用した)メソッドの戻り値を捨てていないこと」の確認はできますが、
Mockフレームワークを使用すると、

  • そのMockが何回呼ばれたか
  • 引数に何が設定されたか

というチェックができますが、これも立派なAssertionの一部です。

例えば、テスト対象メソッドの処理において、
戻り値を返さないメソッド呼び出しがあり、ソースコードの間違った修正で呼び出さなくなった場合、
スタブだと検知することはできませんが、Mockの場合、呼び出し回数が期待値と異なるので想定外だと検知することができます。
「メソッドを呼んでいること」「引数を想定通りに渡していること」という、テスト対象メソッドの重要な責務を確認できるわけです。
上述したようなメソッド分割設計をしておけばMockを用意する箇所は絞り込まれる為、
凝ったMockを作成することが回避できるはずです。

テストクラスを作成する順番

実装したコードを元にテストするな、と言われたことはありませんか?
実装コードを見て、テストケースを作ったら、その通りに動くに決まっています。
そんなテストケースからバグを見つけることは難しく、テストしている意味はありません。
バグを見つける「テスト」の為には、テストクラスを作成する順番が重要です。


テストクラスを作成する時は、「こうあるべき」を先に定義しておき、その振る舞いどおりか「テスト」します。
「こうあるべき」の定義どおりでなかった場合、バグとなるわけです。
そして、テストクラスの想定通りにテスト対象クラスが実装できた後は、
その想定通りの振る舞いをし続けているか「チェック」する為に使用できます。


「チェック」というのは、バグを見つける為ではなく、
メソッドの振る舞いが想定どおりであることを担保する為のものでしかありません。
今後の保守や改修を考えた時に、これがあるとないとでは大違いで、手の入れやすさが違います。
ソースコードを修正したことで、以前のテストクラスが通らなくなってしまった場合、
デグレを起こしてしまった可能性が早期に検知できるわけです。

終わりに

上記を意識してテストクラスを作成するだけで、単体テストの質がぐっと上がる気がします。
テストはテスターのものでなく、開発者のスキルとして確立すれば、
早く帰れるんじゃないかなーなんて思います。
あ、年末年始のお供にぜひ、これを1冊。

*1:もちろん、システムの振る舞いだけでなく、同時更新や大量データの場合等も含みます

*2:Layerの単位は、DBアクセスのように明らかに責務が違うとか、あちこちで呼ばれる処理なんかの場合で分けるとよいと思います

*3:もろトランザクションスクリプトなので、このようなソースはOO厨の方々からは非難轟々かもしれませんが、様々なスキルの人が見ることを考えるとこちらが良いと思われます