Spockのwhen/then/whereで成功と例外の両方をテストする

Javaユニットテストを書くとき、普段は例外が発生するパターンのテストを例外ごとに書き、それとは別に正常終了するテストを書いて、パラメータと結果の組み合わせをfixtureで渡している。

新しくアサインされたJavaプロジェクトでテストを書こうとしたら、テスト対象のメソッドはDAOによるデータ取得とビジネスロジックが交互に行われているトランザクションスクリプトで、DAOをモック化しても大量の前処理が必要だった。

テストフレームワークはSpockを使っているが、Groovyで書いても前処理だけで60行ほど使ってしまうので、成功と例外をまとめて同じテストメソッドで書けないかと思い、試してみたらスマートではないがなんとかできたのでメモ。

状況

前処理で大量にDAOをモック化し、検索結果として取得するインスタンスを用意しないといけない。

省略しているが、既存のテストメソッドでDAOなどを利用しているため、 setup メソッドでのモック化や @Shared での共有もやりづらく、追加したテストメソッド内で完結させたい。

def targetClass = new TargetClass()

// DAOのインスタンス生成

@Unroll
def "targetMethodの例外発生テスト"() {
  given:
  // DAOのモック化、取得するインスタンス生成など

  when:
  targetClass.targetMethod(...)

  then:
  def e = thrown()
  e.class == ...
  e.message == ...
}

@Unroll
def "targetMethod(#param) == #expected のテスト"() {
  given:
  // DAOのモック化、取得するインスタンス生成など
  // テストのパターンによってインスタンスをいじりたい

  expect:
  targetClass.targetMethod(param)

  where:
  param || expected
  ...
}

対応

where で指定するパラメーターに例外クラスを含めておき、 thenthrown で例外を取得。パラメーターで例外が指定されている場合は例外を比較、 null なり _ なりで有効なクラスが指定されていなければメソッド実行結果と期待値を比較、といった判定ができないかと思ったが、 thrown で例外クラスの指定が必要だったため難しかった。

また、 thrownthen の先頭で書かないとエラーになるため、例外クラスを where で書いて判定することもできなかった。

仕方ないので、 whenthen を複数記述し、成功時とエラー時で処理を切り分けることとした。 where で指定したパラメーター数だけ例外発生のテストが重複して実行されてしまうが、ひとまず目的は達成できた。

@Unroll
def "targetMethod(#param)のテスト"() {
  given:
  // DAOのモック化、取得するインスタンス生成など
  def errorParam = ... // エラーが発生するパラメーター

  when:
  def actual = targetClass.targetMethod(param)

  then:
  actual == expected

  when:
  targetClass.targetMethod(errorParam)

  then:
  Exception e = thrown()
  e.message == errorMessage

  where:
  param || expected
  ...   || ...
}

振り返り

既存のソースにテストを追加する形となったため、かなり無理矢理になってしまった。やはりテストを書きやすいよう、データの扱いとビジネスロジックは分離しておくべきだなぁ。

where に例外クラスやメッセージを追加することで、もう少しうまくまとめられるかもしれない。そもそももっとうまいやり方があるかもしれないが、とりあえず目的達成はできたのでここまで。テストに時間をかけすぎるのも本質的ではないし。