KotestのSpring拡張を使う方法になります。
KotestのSpring拡張をdependenciesに追加
io.kotest:kotest-runner-junit5-jvm
だけではなく、Spring拡張のio.kotest.extensions:kotest-extensions-spring
を追加します。
Mockkを使うのに便利なcom.ninja-squad:springmockk
もセットで追加しておきます。
testImplementation("io.kotest:kotest-runner-junit5-jvm:5.1.0")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.0")
testImplementation("com.ninja-squad:springmockk:3.1.0")
テストを書いてみる
テスト対象のコード
テスト対象は、Repositoryを呼び出して結果を詰め替えして返すだけのシンプルな実装としています。
@Transactional(readOnly = true)
class FindUsersUseCase(private val userRepository: UserRepository) {
fun findAllUser(): List<UserDto> {
return userRepository.findAll()
.map { UserDto(it.id, it.name) }
}
}
data class UserDto(val id: Int, val name: String)
テストコード
JUnit と同じように@SpringBootTest
アノテーションを使ってテスト対象のコンポーネントを指定します。
KotestのSpring拡張はこのアノテーションのみでコンストラクターインジェクションを使ってテストが実行できるようです。
テスト対象の中で依存しているRepositoryをモックに差し替えてテストしたいので、dependenciesに追加したcom.ninja-squad:springmockk
の@MockkBean
アノテーションを使用してモックを生成しています。
SpringExtension
をextensions
に登録することで、Kotestのライフサイクルの中でTestExecutionListener
が動作するようになります。(例えばこの機能で、Mockkのモックのクリア処理がテスト実行後に自動的に行われるようになったりします。)
@SpringBootTest(classes = [FindUsersUseCase::class])
@ActiveProfiles("test")
class FindUsersUseCaseTest(
private val sut: FindUsersUseCase,
@MockkBean val mockUserRepository: UserRepository
) : FreeSpec({
extensions(SpringExtension)
"ユーザ一覧が取得できるよ" {
every { mockUserRepository.findAll() } returns listOf(User(1, "name_1"), User(2, "name_2"), User(3, "name_3"))
sut.findAllUser() shouldBe listOf(UserDto(1, "name_1"), UserDto(2, "name_2"), UserDto(3, "name_3"))
verify { mockUserRepository.findAll() }
}
})
SpringExtension
をすべてのテストクラスで登録するのはかなり面倒かつ漏れたりすると思うので、下のようなプロジェクト全体設定で登録しておくといい感じになります。
class ProjectConfig : AbstractProjectConfig() {
override fun extensions() = listOf(SpringExtension)
}
テストの実行結果テストも期待通りに実行できています。

MockMvcを使ったテスト
テストコード
基本的なテストの書き方は、コンポーネントのテストと同じになります。
MockMvcを利用するために、JUnitと同じように@AutoConfigureMockMvc
アノテーションを設定します。
@SpringBootTest
@AutoConfigureMockMvc
internal class UserHandlerTest(
val mockMvc: MockMvc,
val dataSource: DataSource,
val flyway: Flyway
) : FreeSpec({
extension(SpringExtension)
beforeTest {
flyway.clean()
flyway.migrate()
}
"ユーザが登録できるよ" {
val table = Table(dataSource, "user")
val changes = Changes(table)
changes.setStartPointNow()
mockMvc.post("/api/users") {
contentType = MediaType.APPLICATION_JSON
content = """{"name": "siosio"}"""
}.andExpect {
status { isOk() }
}
changes.setEndPointNow()
Assertions.assertThat(changes)
.hasNumberOfChanges(1)
.changeOfCreation()
.rowAtEndPoint().value("name").isEqualTo("siosio")
}
"ユーザ一覧が取得できるよ" {
mockMvc.get("/api/users")
.andExpect {
status { isOk() }
content { contentType(MediaType.APPLICATION_JSON) }
jsonPath("$.users.length()") { value(3) }
jsonPath("$.users[*].name") { value(contains("user_1", "user_2", "user_3")) }
jsonPath("$.users[*].id") { value(contains(1, 2, 3)) }
}
}
}) {
companion object {
@Suppress("unused")
val container = MySQLContainer<Nothing>("mysql:8.0.28").run {
start()
waitingFor(HostPortWaitStrategy())
System.setProperty("spring.datasource.url", jdbcUrl)
System.setProperty("spring.datasource.username", username)
System.setProperty("spring.datasource.password", password)
}
}
}
おわり。