😎

kotlin × Spring × kotest × mockkで書くユニットテスト

2022/06/29に公開

テストの書き方について調べていたときに下記の記事に出会い、はちゃめちゃ勉強になったので実際に現場で使われるであろうkotlin, Springにおいてのテストの書き方を記事に残しておこうと思います。リンクの記事はJavaの話ですが元記事にはkotlinのテストの書き方についても書かれているので興味がある方は読んでみてください。(元記事は英語です)

https://qiita.com/dhirabayashi/items/f3707e5fc9d91cb61880

  1. kotlin × Spring × kotest × mockkで書くユニットテスト <- 今ここ
  2. kotlin × Spring × kotest × testcontainersで書くインテグレーションテスト(Repository編)
  3. kotlin × Spring × kotest × testcontainersで書くインテグレーションテスト(gRPC編)

準備

  • kotlin 1.6.21
  • Java 17
  • Spring boot 2.7.0

Spring Initializrで雛形を作成。
https://start.spring.io/

依存関係はとりあえず下記を選択。(今回はmongodbを使用しますが何でもいいです)

build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springframework.boot:spring-boot-starter-web")

テスティングフレームワークにはkotestを使用します。

build.gradle.kts
testImplementation("io.kotest:kotest-runner-junit5:5.3.1")

ユニットテストに使うモックにはmockkを使用します。

build.gradle.kts
testImplementation("io.mockk:mockk:1.12.4")

テストするクラスの実装

先にドキュメントクラスとRepositoryを作成します。

UserDocument
@Document(collection = "users")
data class UserDocument(
    @Id val id: ObjectId = ObjectId.get(),
    @Field("nm") val name: String = "",
    @Field("age") val age: Int = 0,
    @Field("crtAt") val createdAt: LocalDateTime = LocalDateTime.now(),
    @Field("updAt") val updatedAt: LocalDateTime = LocalDateTime.now()
) {
    constructor(model: User) : this(
        name = model.name,
        age = model.age,
        createdAt = model.createdAt,
        updatedAt = model.updatedAt
    )
}
UserRepository
@Repository
interface UserRepository : MongoRepository<UserDocument, ObjectId> {
}

次にdomain層のクラスを作成します。

User
data class User(
    val id: ObjectId? = null,
    val name: String,
    val age: Int,
    val createdAt: LocalDateTime,
    val updatedAt: LocalDateTime
) {
    constructor(document: UserDocument) : this(
        id = document.id,
        name = document.name,
        age = document.age,
        createdAt = document.createdAt,
        updatedAt = document.updatedAt
    )
}
UserService
@Service
class UserService(
    private val userRepository: UserRepository
) {
    fun create(user: User): User {
        val document = this.userRepository.save(UserDocument(user))
        return User(document)
    }
}

新規でユーザーを作成するだけの処理です。

テストを書く

kotlin, Springを使用したアプリケーションにおいて採用するアーキテクチャにもよるのかもしれないですが筆者はいわゆる3層アーキテクチャ的な作りで書くことが多いです。なので作成するクラスもController, Service, Repositoryに基本的には分類されます。この中でロジック的なものが詰まっていて一番気合いを入れて作るのがServiceクラスになってきます。そして、一番動くか心配で動いてもらわないと困るクラスです。テストをどこまで書くかとかはチームやプロジェクトによるでしょうが何かしらの不安要素がある箇所はテストをすべきだと思っています。

なので、筆者は何かしらの機能を実装した場合はまずサービスクラスのユニットテストを書きます。テスト内容は下記の様な感じ。

UserServiceTest
internal class UserServiceTest : StringSpec({
    lateinit var userRepository: UserRepository
    lateinit var userService: UserService

    beforeTest {
        userRepository = mockk()
        userService = UserService(userRepository)
    }

    "create" {
        //given
        val testTime = LocalDateTime.of(LocalDate.now(), LocalTime.of(0, 0))
        val user = User(name = "user", age = 32, createdAt = testTime, updatedAt = testTime)
        val document = UserDocument(user)

        every { userRepository.save(any()) } returns document

        //when
        val result = userService.create(user)

        //then
        result.shouldBeUser(document)

        verify { userRepository.save(any()) }
        confirmVerified(userRepository)
    }

}) {
    companion object {
        fun User.shouldBeUser(document: UserDocument): User {
            this.id shouldBe document.id
            this.name shouldBe document.name
            this.age shouldBe document.age
            this.createdAt shouldBe document.createdAt
            this.updatedAt shouldBe document.updatedAt
            return this
        }
    }
}

ちょっと微妙だなと思うところもあるのですが以下ポイントみたいなところをまとめます。

internal修飾子をつける

可視性を狭める意味で。ただJavaのときどうしてたっけってなってJavaだとpublicでテスト書いてるのが多くてkotlinだとpublicじゃなくていいのか?privateだと実行もできないからinternalなのか??kotlinのテストを見るとinternalをつけてるのが多いからつけてるけど正確な理由が調べてもわからなかったのでわかる方いたらコメントください!

mock使う

mockを使う辛みもあるのだけどmockを使わないでテストコードの外側のDB環境に依存したテストの辛みの方が筆者は大きかったのでmockを使います。感覚的な話ですがmockを使った方がテストが壊れにくいです。あと、実行が早いケースが多い。ただ、mockは実装者が用意するのでmockの振る舞いが間違っていればテストが崩壊するのであんまり複雑なことはしない方がいいと思う。なるべくシンプルにミスがないように。フレームワークはkotlinであればmockkでいい。mockkがいい。

テストクラスは普通にインスタンス化する

詳細に調べてないですがspringmockkというものもあるのですが@SpringBootTestをなるべく使いたくないのと、コンストラクタインジェクションをしていれば引数にモックインスタンスを指定すればテスト対象のクラスは手に入るので。springmockkも@SpringBootTestも使わない。

ヘルパー関数を使う

1つのテストメソッドをなるべく短いものにしたいので行数を節約できるところはヘルパー関数として切り出すといい。切り出す場所がトップレベルなのかcompanion objectなのか別のクラス的なものなのかいつも悩む。とりあえず、繰り返し出てきそうな処理はどんどん切り出すといい。

まとめ

書くたびにうまく書けないなと思いながらもテスト書いてます。テストはちゃんと書こうとするとホントに難しいなと思うけど、改良の余地がずっとあって割と楽しくなってくるのでどんどんテスト書きましょう!余談ですがkotestがうまく動かなくてJUnitと行ったり来たりしてたけど慣れたらkotestがほんと書きやすい。次はRepositoryのテストについて書こうと思います。

GitHubで編集を提案

Discussion