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]とか付けたかったんですが、一旦諦めてしまいました。