Spockについてまとめてみた

## Spockとは

Spockは、Groovy言語で記述されたBDDスタイルのテストフレームワーク。
JUnit4を基盤としていますが、より高水準のテスト記述が可能で、可読性の高いテストコードの記述が可能。

## Spockの特徴

### BDDスタイルのテストコードの記述が容易

Spockは、BDDスタイルの記述が容易であり、`given-when-then`の記述に基づいて、テストケースを自然な文書化された形式で記述できる。
また、JUnitに比べてテストコードが簡潔で、可読性が高くなるというメリットがある。

### データ駆動テストのサポート

Spockは、データ駆動テストをサポートしている。`where`ブロックを使用して、入力データを定義することができる。
また、`@Unroll`アノテーションを使用することで、入力データに対応するテストケースのラベルを生成することができる。

### モックオブジェクトの簡単な作成

Spockは、Groovy言語に組み込まれているメタプログラミングの機能を使用して、モックオブジェクトを簡単に作成することができる。
また、`Interaction Based Testing`のような形式で、オブジェクト間の相互作用をテストすることができる。

### その他特徴

- 構文がシンプルで読みやすい
- テスト実行速度が速く、JUnitよりも高速
- テストコードを簡潔に書くことができる
- Groovy言語により、Javaよりも簡単に書くことができる

## Spockのコーディング例

### HTTPリクエストのテスト

```groovy
class MySpec extends Specification {

    def "test GET request to /hello returns 'Hello World!'"() {
        given:
        def url = "http://localhost:8080/hello"

        when:
        def response = new URL(url).text

        then:
        response == "Hello World!"
    }

    def "test POST request to /hello with 'John' in request body returns 'Hello John!'"() {
        given:
        def url = "http://localhost:8080/hello"
        def requestBody = ["name": "John"]

        when:
        def response = RestAssured.given()
                      .contentType(ContentType.JSON)
                      .body(requestBody)
                      .post(url)
                      .andReturn()
                      .response
                      .asString()

        then:
        response == "Hello John!"
    }
}
```

上記の例は、Spockを使用してHTTPリクエストのテスト。
1つ目のテストケースでは、URLにGETリクエストを送信し、レスポンスが"Hello World!"であることをアサートしている。
2つ目のテストケースでは、POSTリクエストを送信して、リクエストボディに"name": "John"を含めている



```groovy
class SampleSpockSpec extends spock.lang.Specification {
    def "1 + 1 は 2 であること"() {
        expect:
        1 + 1 == 2
    }
}
```

### Given-When-Then構文

Spockでは、BDDのGiven-When-Then構文に対応しており、テストコードの可読性が向上する。
以下は、SpockでGiven-When-Then構文を使用したテストコードの例。

```groovy
class SampleSpockSpec extends spock.lang.Specification {
    def "入力された値が2である場合、処理結果は4であること"() {
        given:
        def inputValue = 2

        when:
        def result = inputValue * 2

        then:
        result == 4
    }
}
```

### Mockオブジェクトの利用

Spockでは、Mockオブジェクトを利用することができる。
以下は、SpockでMockオブジェクトを利用したテストコードの例。

```groovy
class SampleSpockSpec extends spock.lang.Specification {
    def "SampleServiceの処理結果が正しく返却されること"() {
        given:
        def sampleService = Mock(SampleService)
        sampleService.execute() >> "success"

        when:
        def result = sampleService.execute()

        then:
        result == "success"
    }
}
```

### 例外処理のテスト

以下は、Spockで例外処理をテストするテストコードの例。

```groovy
class SampleSpockSpec extends spock.lang.Specification {
    def "数値が正しくない場合、例外が発生すること"() {
        given:
        def inputValue = "hoge"

        when:
        def result = { Integer.parseInt(inputValue) }

        then:
        def thrownException = thrown(NumberFormatException)
    }
}
```

### 詳細なコーディング例1: データベース接続のテスト

データベース接続を行うクラスをテストする例です。データベース接続が正常に行われているか、例外が発生するかどうかをテストする。

```groovy
import spock.lang.*
import groovy.sql.Sql

class DatabaseConnectionSpec extends Specification {
    def "正常なデータベース接続が行えること"() {
        given:
        def sql = Sql.newInstance("jdbc:mysql://localhost/test", "user", "password", "com.mysql.jdbc.Driver")
        
        when:
        def result = sql.execute("SELECT COUNT(*) FROM users")
        
        then:
        result.rows[0][0] == 3
    }
    
    def "異常なデータベース接続時に例外が発生すること"() {
        given:
        def sql = Sql.newInstance("jdbc:mysql://localhost/unknown", "user", "password", "com.mysql.jdbc.Driver")
        
        when:
        def exception = thrown(Exception)
        def result = sql.execute("SELECT COUNT(*) FROM users")
        
        then:
        exception.message == "Unknown database 'unknown'"
    }
}
```

- `given`ブロックでデータベース接続に必要な情報を準備。
- `when`ブロックでSQLクエリを実行し、結果を取得。
- `then`ブロックで期待する結果をアサート。
- 2つ目のテストケースでは、期待する例外が発生することをアサートするために、`thrown`メソッドを使用。

### 詳細なコーディング例2: REST APIのテスト

REST APIをテストする例です。HTTPリクエストを送信し、レスポンスをアサートする。

```groovy
import spock.lang.*
import io.restassured.RestAssured
import io.restassured.http.ContentType

class RestApiSpec extends Specification {
    def "GETリクエストが成功すること"() {
        when:
        def response = RestAssured.get("https://jsonplaceholder.typicode.com/posts/1")
        
        then:
        response.statusCode == 200
        response.contentType == ContentType.JSON
        response.body.title == "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
    }
    
    def "POSTリクエストが成功すること"() {
        given:
        def body = ["title": "foo", "body": "bar", "userId": 1]
        
        when:
        def response = RestAssured.given()
            .contentType(ContentType.JSON)
            .body(body)
            .post("https://jsonplaceholder.typicode.com/posts")
        
        then:
        response.statusCode == 201
        response.contentType == ContentType.JSON
        response.body.title == "foo"
        response.body.body == "bar"
    }
}
```

- `when`ブロックでHTTPリクエストを送信し、レスポンスを取得。
- `then`ブロックでレスポンスの結果チェックを行う。


### その他コーディング例1: テスト対象クラスのprivateメソッドをテストする

```groovy
class Foo {
    private int add(int a, int b) {
        return a + b
    }
}

class FooSpec extends spock.lang.Specification {
    def "test private method add"() {
        given:
        def foo = new Foo()

        expect:
        foo."$spock_private_method"(a, b) == result

        where:
        a    | b    | result
        1    | 2    | 3
        10   | 20   | 30
        100  | 200  | 300
    }
}
```

上記の例では、テスト対象の `Foo` クラス内に存在する `add` メソッドをテストしている。
Spockではprivateメソッドも直接テストすることができます。テストケースのデータパラメータ化には、`where`ブロックを使用している。

#### その他コーディング例2: 引数の異なる複数のテストケースを一括でテストする

```groovy
class MathUtil {
    static int multiply(int a, int b) {
        return a * b
    }
    static int add(int a, int b) {
        return a + b
    }
}

class MathUtilSpec extends spock.lang.Specification {
    def "test multiply"() {
        expect:
        MathUtil.multiply(a, b) == result

        where:
        a | b | result
        1 | 2 | 2
        5 | 5 | 25
    }

    def "test add"() {
        expect:
        MathUtil.add(a, b) == result

        where:
        a | b | result
        1 | 2 | 3
        5 | 5 | 10
    }
}
```

上記の例では、 `MathUtil` クラスの `multiply` メソッドと `add` メソッドを、複数の異なる引数でテストしています。 `where` ブロックを使うことで、異なる入力値と期待値をまとめて定義し、一括でテストすることができる。

### 参考記述
[【Groovy × Spock】Spockでテストを書いてみよう](https://tech-blog.rakus.co.jp/entry/20221214/spock)
[Spockの基本](https://zenn.dev/yonekubo/books/6f4bde620a7bac/viewer/edecd0)
[JUnit代わりにSpockを使ってみる](https://recruit.gmo.jp/engineer/jisedai/blog/junit-spock/)
[Spockを使ってJavaのテストを効率化する](https://qiita.com/umeki_ryo/items/98336bb8badca6dc11ac)