RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

【JUnit & Spock】テストコードを比較してみる

はじめに

こんにちは、ryrkssです。
今回は、今のプロジェクトのテストフレームワークJUnitSpockが使われているので、
簡単なテストコードでどう書き方が違うのか比較してみようと思います。
また、それぞれ各テストで出てくるアノテーションや検証メソッドについての説明は割愛します。

本記事で使うJUnitJUnit5です。
以後JUnitが多発しますが全てJUnit5として捉えていただければ幸いです。

JUnitとSpockについて

みなさんご存知の方は多いと思いますが「知らないよ」という方に向けてちょっとだけまとめます。

  • JUnit
    Javaのテストフレームワークといえば真っ先に思い浮かぶ有名なテストフレームワークです。
    Javaエンジニアであれば全ての人が通る道ではないでしょうか。

  • Spock
    Groovyで動作するJavaのテストフレームワークです。
    Javaとは違う言語で書くので「違う言語覚えないといけないのか」と思う人もいるかもしれませんが、
    Javaとほぼ同じ感覚でコーディングできます。
    何よりデータドリブンテストがJUnitと比べて非常に書きやすく可読性も高いです。
    あとは、MockAPIが標準で入っていることも嬉しい人には嬉しいと思います。
    ※私は使い慣れているMokitoを使っちゃってますが。

JUnitSpockは共存できるので、是非どちらも試してみてください。
それでは、JUnitSpockのテストコードの書き方を比べてみます。

ライフサイクルの定義

良く使いますがJUnitアノテーションSpockはメソッド名で定義します。

// 各テストメソッドが実行される前に都度実行
@BeforeEach
void init() {}

// 各テストメソッドが実行された後に都度実行
@AfterEach
void cleanup() {}

// テストクラスが実行される前に1度だけ実行
@BeforeAll
static void initAll() {}

// テストクラスが実行された後に1度だけ実行
@AfterAll
static void cleanupAll() {}
  • Spock
    メソッド名で実行タイミングを表現します。
// 各テストメソッドが実行される前に都度実行
def setup() {}

// 各テストメソッドが実行された後に都度実行
def cleanup() {}

// テストクラスが実行される前に1度だけ実行
def setupSpec() {}

// テストクラスが実行された後に1度だけ実行
def cleanupSpec() {}

Spockのメソッド名だけで判別してくれるのは、コードがスッキリして良いですよね。
JUnitはお馴染みのアノテーション定義です。
見慣れすぎて何も思わないです。

テストクラス

  • JUnit
@Test 
@DisplayName("文字列結合")  
void combineStringsTest() { 
    var expected = "テスト";
    var actual = sut.combineStrings("テ", "ス", "ト");
    assertEquals(expected, actual);
}
  • Spock
def "文字列結合"() {
    setup:
    def expected = "テスト"
    when:
    def  actual = sut.combineStrings("テ", "ス", "ト")
    then:
    actual == expected
}

単純なテストを書きましたが、結構違いますね。
JUnitSpockで目に付く違いは、ブロックの定義です。
JUnitはコードを記述しているだけですが、Spockにはsetup:when:など
記載しているもの以外にもそれぞれのフェーズに応じたブロックがあります。
単なる分かりやすくするためだけのものではないので、ちゃんと状況に応じた使い分けをしましょう。

また、こう見るとJUnitアノテーション@Testを毎回つけなければならないのが冗長に感じます。
検証メソッドについてもJUnitはメソッドを使用する必要がありますが、
Spockは等価演算子を使うだけで事足ります。
こちらは比較的万能で配列やリスト、数値なども等価演算子で検証できます。

データドリブンテスト

こちらもよく使います。
同じようなテストを1つ1つメソッドで分けるのは、テストケースが見えにくく可読性が落ちてしまいますよね。
JUnitParameterizedTestで検索すると出てくると思いますが、
渡している値と想定結果の関係性が分かりにくく、同時に書きにくいと感じてしまいました。
その反面Spockは非常に書きやすく、見やすいので実装している時にパターンを洗い出しやすいです。
またレビュー時もテストケースを考えずとも頭に入ってくる感覚で個人的には中毒になるレベルです。

百聞は一見に如かずということで、早速比較してみたいと思います。

  • JUnit

例として、3種類の書き方を記載します。
まだパラメータが少ない且つ、型を同一にしているので幾分見え方がましですが、
②、③についてはパラメータ数が多くなる、色々な型が混じるとなると
本当に何のテストをしているのか分からなくなってきますね…。

/*
 ① 単一パラメータのみを定義する書き方
 引数で定義した型に暗黙的に変換される(サポートされている型は要確認)
*/
@ParameterizedTest
@ValueSource(strings = {" テスト ", " テスト", "テスト "})
void trimTest1(String target) {
    var expected = "テスト";
    var actual = sut.trim(target);
    assertEquals(expected, actual);
}

/*
 ② csv形式でパラメータを定義できる書き方
 デリミタを変更したり外部ファイルを指定することもできる
 例は文字列だけだが他の型も定義できて、引数で定義したそれぞれの型に暗黙的に変換される
*/
@ParameterizedTest
@CsvSource({
      " テスト , テスト",
      " テスト, テスト",
      "テスト , テスト",
})
void trimTest2(String target, String expected) {
    var actual = sut.trim(target);
    assertEquals(expected, actual);
}

/*
 ③ 別メソッドにパラメータを定義できる書き方
 メソッドで引数を定義するので独自の型をパラメータで与えることができたり、
 ただのメソッドなので最終的に返り値の形にしてあげれば何でもできるイメージ
*/
@ParameterizedTest
@MethodSource("trimArguments")
void trimTest3(String target, String expected) {
    var actual = sut.trim(target);
    assertEquals(expected, actual);
}

static Stream<Arguments> trimArguments() {
    return Stream.of(
        Arguments.of(" テスト ", "テスト"),
        Arguments.of(" テスト", "テスト"),
        Arguments.of("テスト ", "テスト"),
        Arguments.of(null, "")
    )
}
  • Spock

例として2種類ありますが、どちらもメソッドに@Unrollをつけて、
where:にパラメータを定義していくだけとなります。
また、文字列だけを扱うテストになってますが、勿論他の型が混じっても問題ありません。
簡単なものしか記載していませんが、
whereは色々な使い方ができるようなので興味のある方は調べてみてください。

/* 
 ①データテーブルを使用した書き方
 1行目がテスト内の変数にマッピングされ、2行目以降がその変数に入れるデータパターン      
 || をつけることで検証用のデータと想定結果を視覚的に分かりやすくできる   
*/
@Unroll("文字列前後空白除去 #title")
def "trimTest1"() {
    when:
    def  actual = sut.trim(target)
    
    then:
    actual == expected
    
    where:
    title | target || expected
    "前後空白" | " テスト " || "テスト"
    "前空白" | " テスト" || "テスト"
    "後空白" | "テスト " || "テスト"
    "null" | null || ""
}

/*
 ②データパイプを使用した書き方
 左シフト演算子で変数とデータ部を接続
*/
@Unroll("文字列前後空白除去 #title")
def "trimTest2"() {
    when:
    def  actual = sut.trim(target)
    
    then:
    actual == expected
    
    where:
    [title, target, expected] << [
        ["前後空白", " テスト ", "テスト"],
        ["前空白", " テスト", "テスト"],
        ["後空白", "テスト ", "テスト"],
        ["null", null, ""]
    ]
}

まとめ

いかがでしたでしょうか?
簡単な例しか出せておらずイメージがつきにくいかもしれませんが、
テストの可読性・書きやすさだけでいうとJUnitよりもSpockの方が印象として良いのかなと私は感じました。
現在のチームでも新しく作るテストクラスは大体Spockで作ってたりしてます。
ただし、JUnitにしかできないこともあります。
例えば、JUnitのネスト(@Nested)を使ってテストをまとめることがSpockではできません。

テストも色々な書き方があって奥が深いですね〜
テストの実装はベストプラクティスをあまり深く考えてこなかったのですが、
しっかりと可読性や何が最良なコーディングかを考えていかなくてはいけないな、と感じております。

全体を通してかなりSpockよりの記事になってしまいましたが、
結論JUnitSpockもどちらも使いやすいです。
最後に...冒頭でも書かせていただきましたがJUnitSpockは共存できるので
是非実際にそれぞれでテストを書いてみてください!


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.