projectormato’s 技術ブログ

Web系ベンチャー企業新卒1年目の技術ブログ

TDD でWordCount してみる(Ruby, Kotlin)

TDDの有名なプラクティスの1つである、Word Countをやってみます。RubyもKotlinもめっちゃ得意じゃないので、何か間違っていたらご指摘ください。

Word Countの仕様は以下のようなものです。

与えられた文字列のそれぞれの単語数を数えて返します。
例)
1. "hello" -> {"hello": 1}
2. "hello hello" -> {"hello": 2}
3. "hello world" -> {"hello": 1, "world": 1}

Ruby

テストから実装していきます。

Ruby: 2.7.0
RSpec: 3.9
RSpec.describe 'Word Count' do
  it 'input one hello' do
    expect(WordCount.new.calc('hello')).to eq({'hello' => 1})
  end
end

これで、RED(エラー)になりますね。 実際にWordCountクラスとcalcメソッドを作成してみます。

class WordCount
  def calc(input)
    {'hello' => 1}
  end
end

これでGREENになります。期待値と同じHashを返すようにしたので、当たり前っちゃ当たり前ですね。テストを満たす最低限の実装をして、リファクタリングをして、その最低限の実装ではREDになるようなテストを書いて...と実装をしていきます。

次のテストを書いてみます。

it 'input two hello' do
    expect(WordCount.new.calc('hello hello')).to eq({'hello' => 2})
  end

これはREDになりますね。GREENになるように実装を書いてみます。

class WordCount
  def calc(input)
    input.split(' ').tally
  end
end

これは冗談です。Ruby2.7.0のリリースノートを眺めていたら、これを書かずにはいられませんでした。Kotlin版はちゃんと書きます!!

Kotlin版

テストを書きます。

Junit: 4.11
Java: 11.0.4
package test

import main.WordCount
import org.junit.Test
import kotlin.test.assertEquals

class WordCountTest {
    @Test
    fun InputOneHello() {
        assertEquals(WordCount().calc("hello"), mapOf("hello" to 1))
    }
}

Ruby版と変わらないテストですね。REDになることを確認できたら、実装をしてみます。

package main

class WordCount {
    fun calc(input: String): Map<String, Int> {
        return mapOf("hello" to 1)
    }
} 

最低限の実装ですね。

次のテストを書いてみます。

    @Test
    fun InputOneWorld() {
        assertEquals(WordCount().calc("world"), mapOf("world" to 1))
    }

最低限の実装をしたいので、こう実装してみます。

    fun calc(input: String): Map<String, Int> {
        if (input == "hello") {
            return mapOf("hello" to 1)
        } else if (input == "world"){
            return mapOf("world" to 1)
        }
        return mapOf()
    }

if文で二つのケースに対応しただけなので、GREENになるはずです。これで、リファクタリングをしてみます。

すごく簡単な例ですし、いきなりこうしてみても、問題はないかもしれません。

fun calc(input: String): Map<String, Int> {
        return mapOf(input to 1)
    }

しかし、これをbaby stepでリファクタリングしてみる事もできます。if文に使っている文字列とreturnに使っている文字列が同じであることに注目して、以下のようにしてみます。

    fun calc(input: String): Map<String, Int> {
        if (input == "hello") {
            return mapOf(input to 1)
        } else if (input == "world"){
            return mapOf(input to 1)
        }
        return mapOf()
    }

これは、すごく自信のある変更です、GREENであるはずです。そうしたら、if文で制御しているのに同じ結果を返しているので、if文は削除できることになります。

fun calc(input: String): Map<String, Int> {
        return mapOf(input to 1)
    }

とても小さなstepを踏んだだけですが、今度はこの変更にとても自信を持てます。意味のなくなったif文を消しただけですからね。

次のテストを追加してみます。

    @Test
    fun inputHelloWorld() {
        assertEquals(WordCount().calc("hello world"), mapOf("hello" to 1, "world" to 1))
    }

まずは最低限の実装をすることに注力します。

    fun calc(input: String): Map<String, Int> {
        if (input == "hello world") {
            return mapOf("hello" to 1, "world" to 1)
        }
        return mapOf(input to 1)
    }

これで、GREENのはずです。

次に、リファクタリングをしてみます。テストに頼りながら、inputをsplitしてmapにつめる実装をしてみましょう。

    fun calc(input: String): Map<String, Int> {
        val result = mutableMapOf<String, Int>()
        for (s in input.split(" ")) {
            result[s] = 1
        }
        return result
    }

良さそうな気がしますね。

さらにテストを追加してみます。

    @Test
    fun inputTwoHello() {
        assertEquals(WordCount().calc("hello hello"), mapOf("hello" to 2))
    }

最低限の実装をします。

    fun calc(input: String): Map<String, Int> {
        if (input == "hello hello") {
            return mapOf("hello" to 2)
        }
        val result = mutableMapOf<String, Int>()
        for (s in input.split(" ")) {
            result[s] = 1
        }
        return result
    }

これで、GREENのはずです。

次に、リファクタリングをしてみます。すでにmapに値がある場合には1を足す処理を追加します。

    fun calc(input: String): Map<String, Int> {
        if (input == "hello hello") {
            return mapOf("hello" to 2)
        }
        val result = mutableMapOf<String, Int>()
        for (s in input.split(" ")) {
            if (result[s] != null) {
                result[s] = result[s]!!.plus(1)
            } else {
                result[s] = 1
            }
        }
        return result
    }

これで、すでにmapに値があるならカウントしていけるようになりました。不要なif文を削除してもGREENであることを確認します。

    fun calc(input: String): Map<String, Int> {
        val result = mutableMapOf<String, Int>()
        for (s in input.split(" ")) {
            if (result[s] != null) {
                result[s] = result[s]!!.plus(1)
            } else {
                result[s] = 1
            }
        }
        return result
    }

単純な実装としては以上でそんなに問題なさそうです。ここまでくれば、だいぶテストケースが揃っているので、かなり強気なリファクタリングをしても、実装に自信を持つことができます。

    fun calc(input: String): Map<String, Int> {
        return input.split(" ").groupingBy { it }.eachCount()
    }

ここまでするかは微妙ですし、もっともっとbaby stepでやった方が良いかもしれません。

今回のコードはこちらにあるので、良かったら参考にしてください。 github.com

本当はコードブロックに[RED]とか[GREEN]とか付けたかったんですが、一旦諦めてしまいました。