はじめに
Javaでのユニットテストにspock frameworkというテスティングフレームワークを使ってみたら
思いのほか便利だったのでご紹介します。
公式サイトはこちら → spock framework
導入するまでの流れが少し解りづらいところがあるので、
最初の導入および spock を使って実際にテストを書いてみるまでを記事にしてみました。
なんとなくでも spock ええやんって思ってもらえると嬉しいです。
ちなみに、筆者環境は Ubuntu 18.04 LTS + eclipse ですが、
Windows 環境でももちろん spock は使えます。そらそうか。
筆者環境
OS: Ubuntu 18.04 LTS
IDE: eclipse
環境作成
groovy 導入
groovy-eclipse 導入
今回はテストコードを groovy で作成しますので、eclipse に groovy を扱うためのプラグインを導入します。
URLはこちらのサイト(groovy-eclipse)を参考にして、使用している eclipse のバージョンに該当するものを使います。
Main Package にチェックを付けて Finish です。
テスト作成
プロジェクトを作る
今回は gradle を使ってサクッと進めていきます。
ソースコードを配置する任意のディレクトリに移動して gradle 実行します。
$ gradle init --type java-application --test-framework spock
以上です。これでユニットテストにspockを使うプロジェクトが出来上がりました。早楽!
既存プロジェクトにspockを追加する
新規でない場合は既存の build.gradle(maven の場合 pom.xml) に記述を追加します。
plugins {
id 'groovy'
}
repositories {
jcenter()
}
dependencies {
testImplementation 'org.codehaus.groovy:groovy-all:2.4.17'
testImplementation 'org.spockframework:spock-core:1.0-groovy-2.4'
testImplementation 'junit:junit:4.12'
}
※ 現時点では groovy-eclipse の導入のときに Main Package のみ導入していれば groovy 2.4 が入っているはずですが、そうでない場合にプロジェクトと eclipse とで groovy のバージョンが合わない場合があります。
バージョンミスマッチのエラーが出るときは下記のページの内容を観てバージョンを合わせる必要があります。
Google Code Archive - Long-term storage for Google Code Project Hosting.
崩れてるので整形したものをおいておきます。
Spock version | Groovy version | JUnit version | Grails version | Spring version |
---|---|---|---|---|
0.5-groovy-1.6 | 1.6.1-1.6.x | 4.7-4.x | 1.2.0-1.2.x | 2.5.0-3.x |
0.5-groovy-1.7 | 1.7.0-1.7.x | 4.7-4.x | 1.3.0-1.3.x | 2.5.0-3.x |
0.6-groovy-1.7 | 1.7.0-1.7.x | 4.7-4.x | 1.3.0-1.3.x | 2.5.0-3.x |
0.6-groovy-1.8 | 1.8.1-1.8.x | 4.7-4.x | 2.0-2.x | 2.5.0-3.x |
0.7-groovy-1.8 | 1.8.1-1.8.x | 4.7-4.x | 2.0-2.x | 2.5.0-3.x |
0.7-groovy-2.0 | 2.0.0 -2.x.x | 4.7-4.x | 2.2-2.x | 2.5.0-3.x |
1.0-groovy-2.0 | 2.0.0 -2.2.x | 4.7-4.x | 2.2-2.x | 2.5.0-4.x |
1.0-groovy-2.3 | 2.3.0 -2.3.x | 4.7-4.x | 2.2-2.x | 2.5.0-4.x |
1.0-groovy-2.4 | 2.4.0 -2.x.x | 4.7-4.x | 2.2-2.x | 2.5.0-4.x |
テストを書いてみよう
テスト駆動のような書き始め
試しにテストを書いてみましょう。
テストの置き場所は src/test/groovy/[package Name]/ です。拡張子は.groovy。
テスト対象クラスと同じパッケージになるようにします。
テストクラス名は[テスト対象のクラス名] + Spec とします。
これはユニットテストと言いつつもテスト対象クラスの仕様(Specification)を表しているからです。
私のこだわりなので、なんでもいいと思います。
それでは、消費税の税込額(8%)を計算してくれるメソッドを実装します。
import spock.lang.Specification
class TaxCalculaterSpec extends Specification {
def sut = new TaxCalculater()
def "calculate: 税込金額を算出する"() {
expect:
sut.calculate(100) == 108
}
}
いきなりテストを書きました。
先程言ったように、これは仕様なのでテストメソッド名は仕様を表すようなものにしました。
この時点ではテストが通る以前にビルドが通りませんので、メインのコードを書きます。
public class TaxCalculater {
public int calculate(int value) {
return 0;
}
}
さて、実行してみると……
Condition not satisfied:
sut.calculate(100) == 108
| | |
| 0 false
はい、テストが見事失敗しました。
そりゃそうだ、0 を返すメソッドなんだから失敗します。
注目ポイントは、このメッセージの出方です。
メソッド実行した結果 0 が返ってきたので 108 との比較が false になってテスト失敗した、
というのがひと目で解るようになっています。この解りやすさが spock の魅力のひとつです。
テスト失敗することを確認したので、テストが通るコードを書きましょう。
public class TaxCalculater {
public int calculate(int value) {
return 108;
}
}
テストが通りました。やったぜ(やってない)
三角測量
テストが1本だけでは、そのテストが本当に妥当なのかどうか解りません。
固定値を返してるだけかも知れないし。よって、値や状況にバリエーションを持たせます。
jUnitだと、こういうときにメソッド呼び出しを増やしたりと
結構重複コードが発生して面倒くさいんですが、spock だともう少し見やすく書けます。
class TaxCalculaterSpec extends Specification {
def sut = new TaxCalculater()
def "税込金額を算出する"() {
expect:
sut.calculate(value) == $result
where:
value |$result
100 |108
200 |216
}
}
where! なんやこいつ! いきなり知らない書き方が出てきました。
この where は markdown っぽく書いてある値の組み合わせを書くことができる記法です。
上記の例ではパターンのぶんだけ"税込金額を算出する"メソッドを繰り返すことを表しています。
2 パターンあるので、このテストメソッドは 2 回実行されます。
この状態でテスト実行するとこんな風に失敗します。
Condition not satisfied:
sut.calculate(value) == $result
| | | | |
| 108 200 | 216
| false
216 が期待値なのに 108 返ってきてるで、ってことですね。そういう実装だもの。仕方ないね。
ということで、実装の方を修正します。
public class TaxCalculater {
public int calculate(int value) {
return (int)(value * 1.08);
}
}
これでテストが通るようになりました。
仕様が追加になったら
先ほどの税込金額を算出するメソッドに仕様を追加してみましょう。
"税込金額に含まれる小数点以下の値は小数点第一位で四捨五入する"という仕様を追加します。
class TaxCalculaterSpec extends Specification {
def sut = new TaxCalculater()
def "税込金額を算出する"() {
expect:
sut.calculate(value) == $result
where:
value |$result
100 |108
200 |216
and: "税込金額に含まれる小数点以下の値は小数点第一位で四捨五入"
111 |120
}
}
where のパターンに追加しました。
111 は 1.08 を掛けると 119.88 ですから、期待値は 120 のはずです。
Condition not satisfied:
sut.calculate(value) == $result
| | | | |
| 119 111 | 120
| false
き、切り捨てされてるゥゥー!!(知ってた)
というわけで四捨五入する処理を入れましょう。
public class TaxCalculater {
public int calculate(int value) {
return (int)(Math.round(value * 1.08));
}
}
このように、仕様が固まったらテストにパターンを追加していきます。
境界値などの仕様も上記のようにテストに書いていくと、とっても見やすいです。
そして、テストを書いておくと勇気を持ってリファクタリングが可能になります。
public class TaxCalculater {
private final static double taxRate = 1.08;
public int calculate(int value) {
return (int)(Math.round(value * taxRate));
}
}
税率を外に出しましたが、テストは無事通りますので問題なしです。
固定値の変数名がキャメルケースなのが気に入らない? ならば修正してテストを回しましょう。
テストが通ってるなら安心です。
例外
特定の条件で例外が発生するというのも仕様のひとつです。
例外が発生することのテストを書いてみます。
def "負の値から税込金額を計算すると例外が発生する"() {
when:
sut.calculate(-1000)
then:
thrown(ApplicationException)
}
解りやすいですね! テスト実行してみましょう。
Expected exception of type 'org.omg.CORBA.portable.ApplicationException', but no exception was thrown
at org.spockframework.lang.SpecInternals.checkExceptionThrown(SpecInternals.java:79)
at org.spockframework.lang.SpecInternals.thrownImpl(SpecInternals.java:66)
at spockSampleProject.TaxCalculaterSpec.負の値から税込金額を計算すると例外が発生する(TaxCalculaterSpec.groovy:27)
ApplicationException が送出されるはずだったのにされてないよ、というテスト失敗です。
public int calculate(int value) throws ApplicationException {
if (value < 0) {
throw new ApplicationException(null, null);
}
return (int) (Math.round(value * taxRate));
}
負の値だったら例外を投げるように実装してテストを流し直すと、すべて成功します。
やったぜ(やったぜ)
※今回は長くならないように端折りましたが、
厳密にやるならば三角測量で書いていくのが良いと思います。
レポーティング(余談)
テストクラスのメソッド名を日本語で仕様を書くことにすると、レポートがかなり見易くなります。
これを見れば仕様が丸わかりというわけです。
個人的には javadoc より断然見やすいので、javadoc 書かなくていいから
ユニットテストをちゃんと書こうと考えています。
最後に
spock framework を使ってテスト駆動っぽいプログラミングをやってみましたが、
いかがでしょうか。
仕様を明確に解りやすく書ける良さが伝わると嬉しいです。
今回紹介した内容以外にも、モッキングのテストを mockit などのモックライブラリを使わずに書けたりとかなりパワフルです。
DbUnit と組み合わせてデータのテストをしたりも、もちろんできます。
これらはそのうち記事にしたいと思います。