KotlinでSpock入門[Spring boot実践編]

この記事は筆者の[ソロ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/panda) 6日目の記事です。

前回までSpockの使用方を紹介してきましたが実際のプロジェクトを想定してSpring bootプロジェクトでのSpockの使用を紹介します。Spockに関しての記事はこれで最後となります。今までの記事で作成したコードは以下になります。

https://github.com/JY8752/spock-demo

[KotlinでSpock入門[基礎編]](https://qiita.com/JY8752/items/5106a268de00a0daa4db)
[KotlinでSpock入門[基礎編2]](https://qiita.com/JY8752/items/c4f7cb585bb91236b975)
[KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す]](https://qiita.com/JY8752/items/e78f7369dff3166b3bb9)
[KotlinでSpock入門[Data Driven Testを学んでデータ駆動テストを使い倒す2]](https://qiita.com/JY8752/items/bc2b1b14fdab36be2570)
[KotlinでSpock入門[mock編]](https://qiita.com/JY8752/items/0bb87522f36a371421c1)
[KotlinでSpock入門[Spring boot実践編]](https://qiita.com/JY8752/items/99ef0c0376b7a2d65510) <- 今ここ

# Repository層のテスト
今回はよくあるController, Repository, Serviceの3層レイヤー構造のアプリケーションを想定してテストを書いていきます。

```kotlin:UserEntity.kt
@Entity
data class UserEntity(
    @Id val id: Long? = null,
    val name: String,
    val age: Int,
    val gender: Gender
)
```

```kotlin:UserRepository.kt
@Repository
interface UserRepository : CrudRepository<UserEntity, Long>
```

上記のUserRepositoryのテストを書く場合、実際にDB環境の用意をする必要があります。DB環境はテスト実行者の環境に直接用意したり、H2といったインメモリの　DB環境を用意するなどやり方は色々あると思いますが筆者はdockerコンテナをコード上で起動するTestcontainersの使用を好んだおり、今回もTestcontainersを使用します！

以下の依存関係を追加します。
```diff_kotlin:build.gradle.kts
+ testImplementation("org.testcontainers:mysql")
+ testImplementation("org.testcontainers:spock:1.17.5")
```

Testcontaienrsの詳しい説明は省略しますが、テストは以下のようになります。

```groovy:UserRepositorySpec.groovy
@Testcontainers
@DataJpaTest(excludeAutoConfiguration = [TestDatabaseAutoConfiguration])
@TestPropertySource(properties = ["spring.jpa.hibernate.ddl-auto=update"])
class UserRepositorySpec extends Specification {
    @Shared
    MySQLContainer container = new MySQLContainer("mysql:latest")
        .withDatabaseName("test")
        .withUsername("test")
        .withPassword("test")

    void setupSpec() {
        container.start()
        System.setProperty('spring.datasource.url', container.jdbcUrl)
        System.setProperty('spring.datasource.username', container.username)
        System.setProperty('spring.datasource.password', container.password)
    }

    @Autowired
    UserRepository userRepository

    def "test"() {
        when:
        def saved = userRepository.save(new UserEntity(1L, "user1", 32, Gender.MEN))
        def find = userRepository.findById(saved.id).orElseThrow()
        then:
        saved == find
    }
}
```

MySQLContainerにdockerイメージ名などを設定してSharedで宣言しておき、setupメソッドでコンテナの起動および起動したコンテナの設定値でシステムプロパティを上書きします。Sharedで宣言するのはテストのたびにコンテナの停止と起動を繰り返すのを防ぐためです。

UserRepositoryの呼び出しは以下の依存を追加することでSpockテスト上でも呼び出すことが可能となります。

```diff_kotlin:build.gradle.kts
+ testImplementation("org.spockframework:spock-spring:2.3-groovy-3.0")
```

# Service層のテスト

```kotlin:UserService.kt
@Service
class UserService(
    private val userRepository: UserRepository
) {
    fun create(name: String, age: Int, gender: Gender): User {
        return this.userRepository.save(
            UserEntity(
                name = name,
                age = age,
                gender = gender
            )
        ).toModel()
    }
}
```

上記のようなUserRepositoryを呼び出し、戻り値であるEntityをModelに変換して返すServiceのテストを想定します。今回はServiceのテストですがRepositoryクラスを呼び出すためDB環境が必要になります。全てのテスト環境にDB接続を強制するのは実行時間やテスト難易度も上がるため今回はRepositoryをスタブにしてテストを書いてみます。

```groovy:UserService.groovy
class UserServiceSpec extends Specification {
    UserRepository mock = Mock(UserRepository)
    UserService service = new UserService(mock)

    def "test create"() {
        given:
        mock.save(_) >> new UserEntity(1L, "user1", 32, Gender.MEN)
        expect:
        service.create("user1", 32, Gender.MEN) == new User("user1", 32, Gender.MEN)
    }
}
```

モック化したRepositoryを引数にUserServiceを直接してインスタンス化して使用するため@SpringBootTestなども必要なく上記のようにテストを書くことができます。

# Controller層のテスト
以下のようなControllerのテストを想定します。

```kotlin:UserController.kt
@RestController
class UserController(
    private val userService: UserService
) {
    @PostMapping("/user")
    fun create(@RequestBody request: CreateUserRequest): User {
        return this.userService.create(request.name, request.age, request.gender)
    }
}

data class CreateUserRequest(val name: String, val age: Int, val gender: Gender)
```

Controllerのテストは以下のようにMockMvcを使用します。Serviceの呼び出しはスタブ化してテストを行います。
```groovy:UserControllerSpec.groovy
class UserControllerSpec extends Specification {
    UserService service = Mock()
    UserController userController = new UserController(service)
    MockMvc mockMvc = MockMvcBuilders.standaloneSetup(userController).build()

    def "test"() {
        given:
        def objectMapper = new ObjectMapper()
        def request = new CreateUserRequest("user1", 32, Gender.MEN)
        service.create(_, _, _) >> new User("user1", 32, Gender.MEN)
        expect:
        mockMvc.perform(
                MockMvcRequestBuilders
                    .post("/user")
                    .content(objectMapper.writeValueAsString(request))
                    .contentType(MediaType.APPLICATION_JSON_VALUE))
                .andExpect(status().isOk())
                .andExpect(content().string("{\"name\":\"user1\",\"age\":32,\"gender\":\"MEN\"}"))
    }
}
```

# まとめ
今回は以下のことについて紹介しました。
- SpringプロジェクトへのSpockの導入について
- Spock, Testcontainersを使用したRepository層のテスト方法
- Spock, Mockを使用したService層のテスト方法
- Spock, MockMvcを使用したController層のテスト方法

他のテスティングフレームワーク同様、SpringプロジェクトにおいてもSpockを導入し、テストができることがわかりました！Spockはテストに仕様書の役割を持たせるために他のテスティングフレームワークにはない書き方や機能が多数あります。Mockが標準的に組み込まれていたり、動的言語であるgroovyだからこその柔軟な書き方があったり、データ駆動テストの書きやすさなども非常に魅力的です。

もし、この記事でSpockやテストに興味を持っていただければ幸いです！