しおしお

IntelliJ IDEAのことなんかを書いてます

KotestのSpring拡張を試してみる

KotestSpring拡張を使う方法になります。

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アノテーションを使用してモックを生成しています。

SpringExtensionextensionsに登録することで、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)
}

テストの実行結果テストも期待通りに実行できています。

f:id:sioiri:20220211214250p:plain

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)
        }
    }
}

おわり。