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, () -&gt; personChecker.isAdult(person));
        assertEquals("Person age should be &gt;= 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
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事