TDDでFizzBuzzを実装してみる

こんにちは。今日はFizzBuzzをTDDで実装していきます。

Fizz Buzzというのは1から順番に数字を発言していき、
3で割り切れる数の時は"Fizz"、5で割り切れる数の時は"Buzz"、3と5の両方で割り切れる数の時は"FizzBuzz"と言っていくゲームです。

1つの数値型の引数を受け取る関数 FizzBuzz を用意し、以下の要件に沿ってTDD × Kotlin × JUnitで実装していきます。
1. 渡された引数をそのまま返す
2. ただし、引数が3で割り切れる時は "Fizz" と返す
3. ただし、引数が5で割り切れる時は "Buzz" と返す
4. ただし、引数が3と5の両方で割り切れる時は "FizzBuzz" と返す

まずテストクラスを作成します。
この時、実装クラスもテスト対象のFizzBuzzメソッドもまだ実装しません。
では、1つ目の要件 渡された引数をそのまま返す のテストケースを1つ書いていきます。

package usecase.main

import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.InjectMockKs
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test


class FizzBuzzUseCaseTest {

    @InjectMockKs
    private lateinit var target: FizzBuzzUseCase

    @Before
    fun setUp() = MockKAnnotations.init(this)

    @Test
    fun `渡された数字を返す`() {
        assertEquals("1", target.fizzBuzz(1))
    }

}

次に実装クラスとメソッドを作成しますが
この時に注意したいのは まだ実装しないこと
TDDでは落ちるテストを書くことから始めるのでまずはテスト対象のメソッドだけ作成し TODO() としておきましょう。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        TODO("未実装だよ")
    }

}

テストを実行してみましょう。
コンソールには以下のエラーが出力されましたが、NotImplementedError となっているので期待通りにテストが失敗しているようですね。 f:id:kkyki:20201202225954p:plain

では、やっと実装していきます。
ここでまた注意しないといけないことは テストが通る最低限の実装 しかしないことです。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        return "1"
    }

}

「あれ?numberを返すんじゃないの? 」 と思うかもしれないですがテストが通る最低限の実装はこれで十分です。
では、テストを実行してみましょう。 f:id:kkyki:20201202230135p:plain
ちゃんとテストが成功したみたいです。

では実装する要件はそのままで、テストケースを追加してテストを実行してみます。

package usecase.main

import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.InjectMockKs
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test


class FizzBuzzUseCaseTest {

    @InjectMockKs
    private lateinit var target: FizzBuzzUseCase

    @Before
    fun setUp() = MockKAnnotations.init(this)

    @Test
    fun `渡された数字を返す_1を受け取って1を返す`() {
        assertEquals("1", target.fizzBuzz(1))
    }

    @Test
    fun `渡された数字を返す_2を受け取り2を返す`() {
        assertEquals("2", target.fizzBuzz(2))
    }

}

テストを実行すると追加したテストだけ落ち、エラーには 期待値は2だけど実際は1が返ってきた と書かれてます。
もちろん今は 1 しか返さないメソッドになっているのでこれも期待通りに落ちているようです。 f:id:kkyki:20201209230927p:plain

では実装を修正していきます。 やっとnumberを使います。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        return number.toString()
    }

}

テストを実行すると、両方とも通りましたー!
f:id:kkyki:20201209212418p:plain

これでやっと1つ目の要件を満たす実装ができましたね。
では次々やっていきましょう。

2つ目の要件は、 引数が3で割り切れる時は "Fizz" と返す でした。
まずはテストを書きましょう。

package usecase.main

import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.InjectMockKs
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test


class FizzBuzzUseCaseTest {

    @InjectMockKs
    private lateinit var target: FizzBuzzUseCase

    @Before
    fun setUp() = MockKAnnotations.init(this)

    @Test
    fun `渡された数字を返す_1を受け取って1を返す`() {
        assertEquals("1", target.fizzBuzz(1))
    }

    @Test
    fun `渡された数字を返す_2を受け取り2を返す`() {
        assertEquals("2", target.fizzBuzz(2))
    }

    @Test
    fun 渡された数字が3で割り切れる時はFizzを返す() {
        assertEquals("Fizz", target.fizzBuzz(5))
    }

}

テストがちゃんと落ちることを確認して(テスト結果は割愛します) 実装をしていきます。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        return if (number % 3 == 0)
            "Fizz"
        else
            number.toString()
    }

}

これで2つ目の要件を満たす実装ができました。
どんどんいきます。

3つ目は 引数が5で割り切れる時は "Buzz" と返すでした。
テストから追加していきましょう。

package usecase.main

import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.InjectMockKs
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test


class FizzBuzzUseCaseTest {

    @InjectMockKs
    private lateinit var target: FizzBuzzUseCase

    @Before
    fun setUp() = MockKAnnotations.init(this)

    @Test
    fun `渡された数字を返す_1を受け取って1を返す`() {
        assertEquals("1", target.fizzBuzz(1))
    }

    @Test
    fun `渡された数字を返す_2を受け取り2を返す`() {
        assertEquals("2", target.fizzBuzz(2))
    }

    @Test
    fun 渡された数字が3で割り切れる時はFizzを返す() {
        assertEquals("Fizz", target.fizzBuzz(3))
    }

    @Test
    fun 渡された数字が5で割り切れる時はBuzzを返す() {
        assertEquals("Buzz", target.fizzBuzz(5))
    }

}

では実装です。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        return if (number % 3 == 0)
            "Fizz"
        else if (number % 5 == 0)
            "Buzz"
        else
            number.toString()
    }

}

Kotlinの余剰はJavaと同じように % を使って計算することができます。

次が最後です。
4つ目の要件は 引数が3と5の両方で割り切れる時は "FizzBuzz" と返す でした。
テストを追加します。

package usecase.main

import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.InjectMockKs
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test


class FizzBuzzUseCaseTest {

    @InjectMockKs
    private lateinit var target: FizzBuzzUseCase

    @Before
    fun setUp() = MockKAnnotations.init(this)

    @Test
    fun `渡された数字を返す_1を受け取って1を返す`() {
        assertEquals("1", target.fizzBuzz(1))
    }

    @Test
    fun `渡された数字を返す_2を受け取り2を返す`() {
        assertEquals("2", target.fizzBuzz(2))
    }

    @Test
    fun 渡された数字が3で割り切れる時はFizzを返す() {
        assertEquals("Fizz", target.fizzBuzz(3))
    }

    @Test
    fun 渡された数字が5で割り切れる時はBuzzを返す() {
        assertEquals("Buzz", target.fizzBuzz(5))
    }

    @Test
    fun 渡された数字が35で割り切れる時はFizzBuzzを返す() {
        assertEquals("FizzBuzz", target.fizzBuzz(15))
    }

}

テストを実行して見ると、 期待値が FizzBuzz で実際の値は Fizz になっています。
f:id:kkyki:20201216213246p:plain

条件文の1つ目 3で割り切れる時は "Fizz" と返す に一致しているので正しくテストが失敗しているようです。
では、最後の実装です。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        return if(number % 3 == 0 && number % 5 == 0)
            "FizzBuzz"
        else if (number % 3 == 0)
            "Fizz"
        else if (number % 5 == 0)
            "Buzz"
        else
            number.toString()
    }

}

はい、これで全ての要件を満たす FizzBuzzメソッドが完成しました!
テストも全て成功しています。 f:id:kkyki:20201216213525p:plain

お疲れ様でした。

ちなみに以下のような書き方もできます。

package usecase.main

class FizzBuzzUseCase {

    fun fizzBuzz(number: Int): String {
        return when {
            number % 3 == 0 && number % 5 == 0 -> "FizzBuzz"
            number % 3 == 0 -> "Fizz"
            number % 5 == 0 -> "Buzz"
            else -> number.toString()

        }
    }
}

こっちの方がコードが短くなって読みやすくなった気がします。
テストがあるので安心してリファクタリングができました。 めでたし!