2019.12.27
JUnit代わりにSpockを使ってみる
こんにちは。次世代システム研究室のB.M.(男性、外国人)です。
最近、Javaの単体テストでJUnitに関するの作業があって、イマイチな点がありました。
そして、前Javaの単体テストでSpockを使用経験があって、JUnitと比べて楽に使える点があって、SpockはJUnitより利点を紹介したいと思います。
JUnitとは
Javaエコシステムで最も人気のある単体テストフレームワークの1つです。
Javaコードのユニットテストというと、まず何はなくともJUnitということになります。
JUnitはJavaのユニットテストのデファクトスタンダードとして歴史も長く、優れたフレームワークではあるが、イマイチな点として
- テスト失敗時に何がどうダメだったかが分かりにくい
- パラメータ化テスト(Parameterized Test)を書くのがめんどくさい&コード見にくい
- パラメータ化テストで失敗したときにどのデータで失敗したのかが分かりにくい
- 標準でモックに対応しておらず、相互作用テストに別途モックライブラリを用意する必要がある
が挙げられています。
Spockとは
Groovyで動作する、テスティングフレームワーク。
Spockは下記の特徴をそなえています
- PowerAssertによる強力なレポーティング (Groovy本体のPowerAssertともまた違うらしい)
- DSLを使った簡潔で分かりやすい記述
- 単純明快なデータドリブンテストの記述が可能
- 標準でモックのAPIを提供
既存のJavaプロジェクトにユニットテストを導入したい人や、JUnitやTestNGでテスト書いてるけど、
もうちょっと楽に書けないかと思ってる人向けの記事です。
以下の実際の単体テストと実行されたの結果で試しましょう。
コードサンプル
クラス | 意味 |
---|---|
Person | 年齢と性別を属性に持ちます |
PersonChecker | Personに対して大人かどうかを判定するisAdultメソッドを持つクラス |
Personクラスは年齢と性別を属性に持つですが、コードが短くためisAdultをテストしてみます。
isAdult関数:
public boolean isAdult(Person person) { return person.age >= 18; }
JUnitのテストコードなら
PersonCheckerTest.java package spockexample; import org.junit.Before; import org.junit.experimental.runners.Enclosed; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; import org.junit.runner.RunWith; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; @RunWith(Enclosed.class) public class PersonCheckerTest { @RunWith(Theories.class) public static class isAdultTest { static PersonChecker sut; @Before public void setup() { sut = new PersonChecker(); } /** * パラメータ化テストのパラメータとなるFixture定義 */ static class Fixture { int age; boolean result; Fixture(int age, boolean expected) { this.age = age; this.result = expected; } } /** * テストに使用するパラメータを定義 */ @DataPoints public static Fixture[] fixtures = { new Fixture(0, "m", false), new Fixture(19, "m", false), new Fixture(20, "m", true), new Fixture(0, "f", false), new Fixture(19, "f", false), new Fixture(20, "f", true), }; @Theory public void testIsAdult(Fixture fixture) { // Fixtureの値を使ってPersonオブジェクトを初期化 Person person = new Person(fixture.age); // テストメソッド実行&結果判定 assertThat(sut.isAdult(person), is(fixture.result)); } } }
Spockのテストコードなら
PersonCheckerSpec.groovy package spockexample import spock.lang.Specification import spock.lang.Unroll; class PersonCheckerSpec extends Specification { @Unroll def "#age歳で性別が#sexの場合に大人かどうかの判定で#resultが返る"() { setup: def sut = new PersonChecker() expect: sut.isAdult(new Person(sex, age)) == result where: age | sex || result 0 | "m" || false 19 | "m" || false 20 | "m" || true 0 | "f" || false 19 | "f" || false 20 | "f" || true } }
なんだか短くてわかりやすいですね。
Spockの書き方は簡単に説明いたします:
- setup、expect、whereでコードの各ブロックを区別して読みやすくなります。
- @Unroll使う時はパラメータをdefしないように気をつけます。
- ==:SpockのPower Assertで、JUnitのassertXXXです。
- whereのところはData Tablesです。Data Tablesは条件と結果を書くところです。
- ||:条件と結果の区別のです。
両方を見るとSpockの方が短くなって、コードは理解しやすいと感じられます。
Spockの書き方はwhereのところに条件と結果はテーブル形で表示されています。こんな書き方なら、条件と結果はわかりやすく見えます。どんな条件でどんな結果が出るのは想像しやすいですから、新しいテストケースを追加するときもとても楽です。
JUnitとSpockのテスト結果比較
JUnitのテスト結果
上で書いたのJunitコードはわざとパラメータを変えて失敗させてみた場合、
こんな出力になります。
期待された結果と、実際の値の比較のみが
Expected: is <false> but: was <true>
しか無いですね。
Spockのテスト結果
上で書いたのSpockコードはわざとパラメータを変えて失敗させてみた場合、
こんな出力になります。
SpockのPowerAssert機能で、どの変数、どの式にどんな値が入っているか、なぜテストに失敗してしまったかが情報がいっぱいでわかりやすく提供されています。
上の例でメソッド名のところに文字列を書いているが、メソッド名を文字列にし、パラメータを埋め込むことで、実行時にメソッド名が動的に評価され、結果をわかりやすく表示することができます。
各メソッド名は同期的で設定されていて、読むのは楽になり、エラーをチェックするのもしやすくなります。
JUnitとSpockの更なる比較
Stringの比較
以下のは”Hello GMO!”と”Hello GMOクリック証券!”を比較しましたら、62%同じの結果も出ました。
例外のテスト
JUnitの例外のテスト
import static org.junit.jupiter.api.Assertions.*; // Gradleのjupiterを追加必要です。 @Test @DisplayName("test with exception") public void shouldThrowExceptionOnIsAdult() { //when PersonChecker personChecker = new PersonChecker(); Person person = new Person(-1, "m"); // isAdultで年齢は0歳以上の条件が設定されている //then RuntimeException thrown = assertThrows(RuntimeException.class, () -> personChecker.isAdult(person)); assertEquals("Person age should be >= 1001", thrown.getMessage()); } 実行結果: <img class="aligncenter size-large wp-image-5894" src="https://www.gmo-jisedai.com/wp-content/uploads/スクリーンショット-2019-12-27-12.15.23-1024x322.png" alt="" width="1024" height="322" />
Spockの例外のテスト
// Spockをbuild.gradleに追加したら何もインポートするのが必要ないです def "例外のテスト"() { setup: // テスト対象の初期化 def personChecker = new PersonChecker() def person = new Person(-1, "m") when: personChecker.isAdult(person) then: // IllegalArgumentExceptionがスローされるはず def ex = thrown(RuntimeException) // Exceptionのメッセージは「Person age should be >= 0」のはずが、わざとエラー出るようにします。 ex.getMessage() == "Person age should be >= 100" }
thrown(例外クラス)だけでスローされる例外のクラスが判定出来ます。
それ以上のチェックをするなら、thrownメソッドが発生したThrowableを返すので、それをチェックすればよいです。
テスト失敗のエラーのログ:
わかりやすく設定してくれましたね。
Spockの弱点
色々利点がありましたが、では弱点はありますか?もちろんあります。
- 速度:上記の最初の実行結果の写真を見ましたらJUnitは16msがかかりしたが、Spockは240msもかかりました。ですが、テストのエラーの情報が充実なので、バグは早く治るのはメリットですしょう。
- JUnitからSpockを馴染める時間がかかります。
もう少し詳しく
Spockのテストクラスは、spock.lang.Specificationクラスを継承して作成します。
実際的なテストの中身を定義したメソッドをfeatureメソッドと呼びます。
つまり上記の例はすべてfeatureメソッドです。
その他に、各featureメソッドに共通の前後処理を記述するためのメソッドがあります。
メソッド | 内容 | JUnitで言うと |
---|---|---|
def setup() | 各featureメソッドの前に実行される | @Beforeアノテーション |
def cleanup() | 各featureメソッドの後に実行される | @Afterアノテーション |
def setupSpec() | 最初のfeatureメソッドの前に1度だけ実行される | @BeforeClassアノテーション |
def cleanupSpec() | 最後のfeatureメソッドの後に1度だけ実行される | @AfterClassアノテーション |
ブロック
featureメソッド内のブロック(setup:とかwhere:とか)は、下記のような種類があります。
ブロック | 意味 |
---|---|
setup: or given: | オブジェクトの生成、データの初期化などの準備。(givenはsetupのエイリアス) |
when: | テスト対象を実行 |
then: | when実行後の結果の検証 |
expect: | テスト対象の実行と結果の検証を同時に |
cleanup: | テストで作成されたデータの削除などの後始末処理 |
where: | データ駆動テストを書くときのパラメータ定義 |
whenとthenはセットで使います。
when -> then -> when -> then …と繰り返すことはOKです。
whenとthenの間にexpectを置くこともできないです。
結論
- Groovyは導入するのはそんなに大変なことじゃないでしょう。
- Spockで宣言的かシンプルに多くのパタンを網羅します。
- PowerAsertでテスト失敗時にも直感的なエラーが表示されていまするのはJUnitより良いでしょう。
- JUnit 5はまだとても流行っているの単体テストのフレームワークです。
- 使用する人が多くて、サポートの人も多くて、これからJUnit 5はSpockのユニークのフィーチャーがあると思います。
- ですが、Spockでもとても良い単体テストの フレームワークです。
- 絶対の一番良いのフレームワークがないです。環境、状態により使用した方が良いと思います。
今回は、まだSpockの使用方を紹介するのが長すぎかもしれません。ですので、また今度、引き続き紹介したいと思います。
参考したリンク:
https://www.jfokus.se/jfokus19-preso/Spock-vs-JUnit-5–Clash-of-the-Titans.pdf
https://www.slideshare.net/disc99_/javagroovy-65909637
https://qiita.com/euno7/items/1e834d3d58da3e659f92
https://www.blazemeter.com/blog/spock-vs-junit-which-one-should-you-choose/
最後に
次世代システム研究室では、アプリケーション開発や設計を行うアーキテクト、またはブロックチェーンのエンジニアを募集しています。次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD