KotlinではじめるBDD

こんにちは。NewsPicksエンジニアの西山です。

9月22日に開催された「Server-Side Kotlin Study #2」に登壇させていただいたので、今回はこちらの内容に関して紹介させていただければと思います。発表資料はこちらです。 speakerdeck.com

Server-Side Kotlin Studyとは?

Server-Side Kotlinで開発を進める中で得られた知見や落とし穴などを共有し、さらにServer-Side Kotlinを盛り上げていくことを目的としている勉強会です。

執筆時までに2回開催されており、内容は全てYouTube上で公開されているので興味がある方はぜひ一度見て見てください。

発表内容

今回発表させていただいたのは、NewsPicksにて決済機能をマイクロサービス化した際のテストに関するものです。詳しくはリンク先のYouTubeで公開されているので、ここでは簡単に内容を紹介させていただきます。

背景

私が所属しているチームでは、有料会員向けの機能開発を行っています。その一環で決済機能をマイクロサービスに切り出したのですが、そこでテストを高速で回していくための工夫を話させていただいています。 マイクロサービス化する以前はスプレッドシートで管理されているテストケースを実行していく形でテストをしていました。しかしながら決済機能のテストということで、テストケースは複雑かつ膨大であり、メンテナンスも大変です。

そこで導入させていただいたのがCucumberというBDD(Behavior Driven Development)をサポートするライブラリです。 今回は私のチームではBDD( Behavior Driven Development)をサポートするためのツールとしてCucumberを導入しました。初めはテストの高速化や管理をしやすくするために導入したのですが、実際に導入してみるとそれ以上の効果を実感できました。

BDDとは?

BDDとは、「Behavior Driven Development」の略で、ソフトフェア開発手法の一つです。 開発の流れとしては下記のようになります。

  1. 新機能の具体的な例を話して、その機能で期待していることの詳細を探り、合意する
  2. それらの例を自動化できる方法で文書化し、一致するか確認する
  3. 文書化された各例で記載されている動作を実装し、コードの開発の指針となる自動テストからはじめる。

Cucumberについて

今回はSpringとKotlinで作成する簡単なタスク管理APIを想定してテストシナリオを書いています。まずはプロジェクトでcucumberを使用できるように下記のように依存関係とCucumberのタスクをbuild.gradle.ktsに追記します。

val cucumberVersion = "6.11.0"
dependencies {
    testImplementation("io.cucumber:cucumber-java:${cucumberVersion}")
    testImplementation("io.cucumber:cucumber-junit:${cucumberVersion}")
    testImplementation("io.cucumber:cucumber-spring:${cucumberVersion}")
}

task("cucumber") {
    dependsOn("assemble", "testClasses")
    doLast {
        javaexec {
            main = "cucumber.api.cli.Main"
            classpath = cucumberRuntime + sourceSets.main.get().output + sourceSets.test.get().output
            args = listOf("--plugin", "pretty", "--plugin", "html:target/cucumber-report.html", "--glue", "com.example.cucumberkotlin", "src/test/resources")
        }
    }
}

これでCucumberを使用する準備は完了です。

続いて自然言語でテストシナリオを記載していきます。

Feature: タスク
  タスクの操作に関するシナリオテスト

  Scenario: タスクの登録
    When タスク「テスト」を登録する
    Then タスク名「テスト」、ステータス「OPEN」のタスクの登録が成功していること

最後に各ステップ(Given / When / Then)で実行される処理をStepファイルに記載していきます。 {word} はステップ内で変数を使用するためのもので、他にも数字を扱いたい場合は {int} など、さまざまなものに対応しています。

class TaskStep(
    private val mockMvc: MockMvc,
    private val taskService: TaskService
) {

    private lateinit var result: ResultActions

    @When("タスク「{word}」を登録する")
    fun registerTask(taskName: String) {
        val requestBody = "{\"name\":\"${taskName}\"}"
        result = mockMvc.perform(post("/task").content(requestBody).contentType("application/json"))
    }

    @Then("タスク名「{word}」、ステータス「{word}」のタスクの登録が成功していること")
    fun assertTask(taskName: String, status: String) {
        result.andExpect(status().isOk)
            .andExpect(jsonPath("$.taskName").value(taskName))
            .andExpect(jsonPath("$.status").value(status))
    }

これでテストを実行する準備はできました。あとはこのテストが通るようにAPIを作成していくだけです。

まとめ

私たちがいるチームではCucumberのテストをGitHub上でPRができるたびに実行するようにしています。こうすることによって大胆なリファクタリングをしても既存機能への影響をほとんど気にすることなく開発することができるようになりました。 また、このテストケースを管理し続けることによって、チームにメンバーが増えた時の仕様の共有もかなりスムーズになりました。

最初の導入コストは多少かかるものの、チームとしては導入することでかなり開発効率が上がったと実感しています。

おわりに

ユーザベースではエンジニアを募集しています。ご興味ある方いらっしゃいましたらこちらからぜひご応募いただければと思います!

© Uzabase, Inc. All rights reserved.