TDD 序

序として選んだのは「テストコードをGroovyで、製品コードをJavaで書く」というものです。

テスティングフレームワークにはSpockを使用します。また、実行環境にはJava SE 7、Groovy 1.8.5を用います。

KeyValueStoreSpec.groovyを作成

まずはspecファイルを作成します。

+package xutp
+
+import spock.lang.*
+
+class KeyValueStoreSpec extends Specification {
+
+}

まだテストコードは何も書いていませんが、実行してみましょう。

Process finished with exit code 0

なお、IntelliJ IDEAを使用しているのであれば、テスト実行結果はこのような形になります。 (実行結果のスクリーンショット)

最初のテストを書く

一番最初のテストコードを書きましょう。

givenブロックに設定作業、whenブロックに実行、thenブロックに期待する応答を記述していきます。whenとthenは必ずペアで記述することになります。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

+   def "空のKVSにput('aaa','bbb')するとget('aaa')で'bbb'が取得できる" () {
+
+       given:
+       def kvs = new KeyValueStore()
+
+       when:
+       kvs.put('aaa','bbb')
+
+       then:
+       kvs.get('aaa') == 'bbb'
+   }
}

空のkvsが与えられ、’aaa’と’bbb’のペアをputしたとき、’aaa’でgetすると’bbb’が返ってくることを確認するテストを書きました。 実行してみましょう。

unable to resolve class KeyValueStore

KeyValueStoreクラスがないと怒られました。確かに、まだ作っていないですね。作りましょう。

KeyValueStoreクラスの作成

+package xutp;
+
+public class KeyValueStore {
+
+}

実行してみましょう。

groovy.lang.MissingMethodException: No signature of method: KeyValueStore.put() is applicable for argument types: (java.lang.String, java.lang.String) values: [aaa, bbb]
Possible solutions: putAt(java.lang.String, java.lang.Object), wait(), dump(), any(), find(), grep()
  at KeyValueStoreSpec.空のKVSにput('aaa','bbb')するとget('aaa')で'bbb'が取得できる(KeyValueStoreSpec.groovy:11)

Process finished with exit code -1

テストはまだ落ちたままです。KeyValueStoreにはput(String,String)なシグネチャのメソッドはないよ、と言われました。putを作りましょう。

putメソッド作成

package xutp;

public class KeyValueStore {

+    public void put(String key, String value) {
+
+   }
}

実行してみましょう。

groovy.lang.MissingMethodException: No signature of method: KeyValueStore.get() is applicable for argument types: (java.lang.String) values: [aaa]
Possible solutions: getAt(java.lang.String), grep(), put(java.lang.String, java.lang.String), grep(java.lang.Object), wait(), any()
  at KeyValueStoreSpec.空のKVSにput('aaa','bbb')するとget('aaa')で'bbb'が取得できる(KeyValueStoreSpec.groovy:14)

Process finished with exit code -1

今度はKeyvalueStoreにgetメソッドはないよと言われました。これも作りましょう。

getメソッド作成

package xutp;

public class KeyValueStore {

    public void put(String key, String value) {

    }
+
+   public String get(String key) {
+       return null;
+   }
}

実行してみましょう。

Condition not satisfied:

kvs.get('aaa') == 'bbb'
|   |          |
|   null       false
KeyValueStore@99a6440

at KeyValueStoreSpec.空のKVSにput('aaa','bbb')するとget('aaa')で'bbb'が取得できる(KeyValueStoreSpec.groovy:14)

Process finished with exit code -1

‘bbb’が返ってきてほしいところにtextが返ってきました。では、テストに通るように実装しましょう。

getの仮実装

テストコードのテストをやっておきたいので、仮実装でテストが通るかどうか確認します。

package xutp;

public class KeyValueStore {

    public void put(String key, String value) {

    }

    public String get(String key) {
-       return null;
+       return "bbb";
    }
}

実行してみましょう。

Process finished with exit code 0

仮実装してテストに通ったので、これでテストのテストが完了しました。安心してテスト実行できそうです。

三角測量

このままだと実装が安易過ぎるので、別のデータを使ったテストを追加しましょう。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def "空のKVSにput('aaa','bbb')するとget('aaa')で'bbb'が取得できる" () {

        given:
        def kvs = new KeyValueStore()

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('aaa') == 'bbb'
    }

+   def "空のKVSにput('ccc','ddd')するとget('ccc')で'ddd'が取得できる" () {
+
+       given:
+       def kvs = new KeyValueStore()
+
+       when:
+       kvs.put('ccc','ddd')
+
+       then:
+       kvs.get('ccc') == 'ddd'
+   }
}

実行してみましょう。

Condition not satisfied:

kvs.get('ccc') == 'ddd'
|   |          |
|   bbb        false
|              3 differences (0% similarity)
|              (bbb)
|              (ddd)
kvs.KeyValueStore@2efe918f

at kvs.KeyValueStoreSpec.空のKVSにput('ccc','ddd')するとget('ccc')で'ddd'が取得できる(KeyValueStoreSpec.groovy:29)

Process finished with exit code -1

もちろん、仮実装で書いたコードではテストに通りません。そろそろきちんと実装しましょう。

明白な実装

やや一足飛びですが、Mapを利用してkeyとvalueのペアを記憶し、指定のkeyに対応するvalueを取得して返すようにしましょう。

package xutp:

+import java.util.Map;
+import java.util.LinkedHashMap;
+
public class KeyValueStore {
   
+   private final Map<String,String> store = new LinkedHashMap<>();
    
    public void put(String key, String value) {
+       store.put(key,value);
    }
      
    public String get(String key) {
-       return "bbb";
+       return store.get(key);
    }
}

GroovyのMapリテラルはLinkedHashMapになるので、製品コード側もLinkedHashMapがいいな、ということでLinkedHashMapを選択しています。

実行しましょう。

Process finished with exit code 0

テストに通りました。

テストコードのリファクタリング

作成した2つのテストメソッドを見比べてみると、putやget、そして期待値のデータが異なるだけで構造は重複しています。 parameterized testの機能を使ってリファクタリングしましょう。

Spockではwhere句で変数名とパラメータを指定できます。 (今回は変更後のコードのみ記載します)

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        given:
        def kvs = new KeyValueStore()

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }
}

重複もなくなり、随分とすっきりしましたね。実行してみましょう。

Process finished with exit code 0

put対象のkeyがtextなら例外発生

今回の仕様では、put対象のkeyがtextだと例外が発生してほしいようです。まずはテストを書きます。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        given:
        def kvs = new KeyValueStore()

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

+   def "KVSにnullのkeyをputすると例外が発生する" () {
+
+       given:
+       def kvs = new KeyValueStore()
+
+       when:
+       kvs.put(null,'null')
+
+       then:
+       thrown(IllegalArgumentException)
+   }
}

実行してみましょう。

Expected exception java.lang.IllegalArgumentException, but no exception was thrown
  at spock.lang.Specification.thrown(Specification.java:232)
  at KeyValueStoreSpec.KVSにnullのkeyをputすると例外が発生する(KeyValueStoreSpec.groovy:32)

Process finished with exit code -1

IllegalArgumentExceptionを期待していたのに、何も例外が投げられなかったためテストに失敗しました。期待通りですね。

それでは、このテストに通るように変更しましょう。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
+       if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
        return store.get(key);
    }
}

実行してみましょう。

Process finished with exit code 0

setupメソッドの抽出

kvsのインスタンス作成部分が重複していますね。各テストの実行前に毎回実行されるsetupメソッドに抽出して重複を排除しましょう。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

+   def kvs

+   def setup() {
+       kvs = new KeyValueStore()
+   }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

-       given:
-       def kvs = new KeyValueStore()

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

-       given:
-       def kvs = new KeyValueStore() 

        when:
        kvs.put(null,'null')
 
        then:
        thrown(IllegalArgumentException)
    }
}

実行してみましょう。

Process finished with exit code 0

getにtextを渡すと例外発生

putと同様に、getに渡すkeyがtextだと例外が発生してほしいようです。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')
				
        then:
        thrown(IllegalArgumentException)
    }

+   def "KVSに対してnullでgetを行うと例外が発生する" () {
+
+       given:
+       kvs.put('aaa','bbb')
+
+       when:
+       kvs.get(null)
+
+       then:
+       thrown(IllegalArgumentException)
+   }
}

実行してみましょう。

Expected exception java.lang.IllegalArgumentException, but no exception was thrown
  at spock.lang.Specification.thrown(Specification.java:232)
  at KeyValueStoreSpec.KVSに対してnullでgetを行うと例外が発生する(KeyValueStoreSpec.groovy:45)

Process finished with exit code -1

IllegalArgumentExceptionが投げられなかったためテストに失敗しました。テストに通るように変更しましょう。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
+       if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }
}

実行してみましょう。

Process finished with exit code 0

kvsに登録されていないkeyでgetを実行するとtextが返る

この仕様に関しては、LinkedHashMapのgetメソッドが同様の動作をするので特に不安はありません。 しかし、テストコードに仕様書としての側面も持たせたいので、今回はテストを書いておきましょう。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

+   def "KVSに登録されていないkeyでgetを行うとnullが返る" () {
+
+       when:
+       kvs.put('aaa','bbb')
+
+       then:
+       kvs.get('other') == null
+   }
}

実行してみましょう。

Process finished with exit code 0

予想通り、テストが成功しました。

既に登録されているkeyに対してputした場合はvalueのみ更新

これもLinkedHashMapのputメソッドが同様の動作をしますが、テストを書いておきましょう。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

+   def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {
+
+       given:
+       kvs.put('aaa','bbb')
+
+       when:
+       kvs.put('aaa','ccc')
+
+       then:
+       kvs.get('aaa') == 'ccc'
+   }
}

実行してみましょう。

Process finished with exit code 0

deleteのテスト

ここからは、kvsから指定のkeyとvalueを削除するdeleteメソッドを作成していきます。 まずはテストからなのですが、今回は複数のデータでテストを行うことが前提なので、あらかじめparameterized testを使って書きます。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')
        
        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

+   def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {
+
+       given:
+       kvs.put(key,value)
+       
+       when:
+       kvs.delete(key)
+        
+       then:
+       kvs.get(key) == null
+       
+       where:
+       key   | value
+       'aaa' | 'bbb'
+   }
}

実行してみましょう。

groovy.lang.MissingMethodException: No signature of method: KeyValueStore.delete() is applicable for argument types: (java.lang.String) values: [aaa]
Possible solutions: get(java.lang.String), getAt(java.lang.String), every(), sleep(long), every(groovy.lang.Closure), split(groovy.lang.Closure)
  at KeyValueStoreSpec.既にKVSに存在するkeyをdeleteするとkeyとvalueのペアがKVSから削除される(KeyValueStoreSpec.groovy:68)

Process finished with exit code -1

deleteがないのでエラーが発生しています。実装してしまいましょう。

deleteの実装

LinkedHashMap.removeを利用すれば仕様を満たすので、一気に実装してしまいます。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }
+
+   public void delete(String key) {
+       store.remove(key);
+   }
}

実行してみましょう。

Process finished with exit code 0

他のデータでも仕様どおり動くか試したいので、データを追加してみます。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')
    
        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
        
        where:
        key   | value
        'aaa' | 'bbb'
+       'ccc' | 'ddd'
    }
}

実行してみましょう。

Process finished with exit code 0

dumpの実装

kvsに登録されているkeyとvalueの一覧を文字列で返すdumpメソッドを実装します。

dumpのテスト

どのような形で出力するかは仕様に書かれていないので、今回はこちらで”{key1:value1}{key2:value2} ...”となるように実装したいと思います。

まずテストを書きましょう。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

+   def "KVSに登録されているkey-valueを文字列で取得できる" () {
+
+       when:
+       kvs.put('aaa','bbb')
+
+       then:
+       kvs.dump() == "{aaa:bbb}"
+   }
}

実行してみましょう。

Condition not satisfied:

kvs.dump() == "{aaa:bbb}"
|   |      |
|   |      false
|   |      33 differences (17% similarity)
|   |      (<KeyV)a(lueStore@66557791 store=[)aa(a):bbb(]>)
|   |      ({----)a(-------------------------)aa(-):bbb(}-)
|   <KeyValueStore@66557791 store=[aaa:bbb]>
KeyValueStore@66557791

at KeyValueStoreSpec.KVSに登録されているkey-valueを文字列で取得できる(KeyValueStoreSpec.groovy:103)

Process finished with exit code -1

GroovyのObjectクラスに存在するdumpメソッドが文字列を返したようですが、期待値と一致しないためテストが失敗しました。

一歩が大きいので小さくする

ここでふと、いきなりdumpの実装は大きすぎた気がしてきたので、メソッドを細かくすることにしました。

dumpのテストには@Ignoreアノテーションをつけてひとまず横に置いておき、keyとvalueを渡すと{key:value}という文字列が返るdebugPrintメソッドを作りましょう。まずはテストコードです。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }
  
    def "KVSに対してnullでgetを行うと例外が発生する" () {
        
        given:
        kvs.put('aaa','bbb')
        
        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

+   @Ignore
    def "KVSに登録されているkey-value一覧を文字列で取得できる" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.dump() == '{aaa:bbb}'
    }

+   def "keyとvalueをdebugPrintに渡すと{key:value}の形で文字列を返す" () {
+
+       expect:
+       kvs.debugPrint('aaa','bbb') == '{aaa:bbb}'
+   }
}

Spockでは、実行と期待値を一つの式に記述するのが場合、thenよりもexpectブロックの利用が好まれます。

実行しましょう。

Test '.KeyValueStoreSpec.KVSに登録されているkey-value一覧を文字列で取得できる' ignored

groovy.lang.MissingMethodException: No signature of method: KeyValueStore.debugPrint() is applicable for argument types: (java.lang.String, java.lang.String) values: [aaa, bbb]
  at KeyValueStoreSpec.keyとvalueをdebugPrintに渡すと{key:value}の形で文字列を返す(KeyValueStoreSpec.groovy:98)

Process finished with exit code -1

dumpのテストが無視されていること、debugPrintメソッドは存在しないことがわかります。

debugPrintの実装

簡単そうなので一気に実装してしまいましょう。

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }

    public void delete(String key) {
        store.remove(key);
    }                                       
+   private String debugPrint(String key, String value) {
+       StringBuilder result = new StringBuilder();
+       result.append("{");
+       result.append(key);
+       result.append(":");
+       result.append(value);
+       result.append("}");
+       return result.toString();
+   }
}

実行してみましょう。

Test '.KeyValueStoreSpec.KVSに登録されているkey-valueを文字列で取得できる' ignored

Process finished with exit code 0

現バージョン(1.8.5)のGroovyではprivateなメソッドに簡単にアクセス可能なので、テストは成功します。 ただし、この機能は一種のバグなので今後のバージョンでは使えなくなる可能性 もあります。 ですので、本機能はローカルな範囲での一時利用に限定して使うのが無難です。 例えば、今回のような使い捨てテストコードを書くとか、リファクタリングする際などです。

dumpのテストに戻る

それではdumpに戻りましょう。dumpのテストにつけていた@IgnoreとdebugPrintのテストを削除します。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {
        
        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

-   @Ignore
    def "KVSに登録されているkey-valueを文字列で取得できる" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.dump() == '{aaa:bbb}'
    }

-   def "keyとvalueをdebugPrintに渡すと{key:value}の形で文字列を返す" () {
-
-       expect:
-       kvs.debugPrint('aaa','bbb') == '{aaa:bbb}'
-   }
}

これでdumpのテストに失敗した状態まで戻りました。

改めて、dumpの実装

Map.entrySetメソッドで取得したSetの各key-valueに対して、をdebugPrintで文字列を取得すれば実装できそうですね。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }

    public void delete(String key) {
        store.remove(key);
    }

    private String debugPrint(String key, String value) {
        StringBuilder result = new StringBuilder();
        result.append("{");
        result.append(key);
        result.append(":");
        result.append(value);
        result.append("}");
        return result.toString();
    }

+   public String dump() {
+       StringBuilder result = new StringBuilder();
+       for(Map.Entry<String,String> pair : store.entrySet()) {
+           String keyvalue = debugPrint(pair.getKey(), pair.getValue());
+           result.append(keyvalue);
+       }
+       return result.toString();
+   }
}

実行してみましょう。

Process finished with exit code 0

無事にテストが通りました。

不安なので複数登録されている場合もテスト

key-valueが1つ登録されている場合のdumpテストを、2つ登録されている場合のテストに変更します。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSに登録されているkey-valueを文字列で取得できる" () {

        when:
        kvs.put('aaa','bbb')
+       kvs.put('ccc','ddd')

        then:
-       kvs.dump() == '{aaa:bbb}'
+       kvs.dump() == '{aaa:bbb}{ccc:ddd}'
    }
}

実行してみましょう。

Process finished with exit code 0

debugPrintの処遇

debugPrintにメソッドのインライン化を適用すべきかどうかが悩ましいところです。

現状、いつフォーマットが変更されるかわからないのでこのままにしておきますが、フォーマットが変更されないと判明した時点でdumpメソッドに展開してしまいましょう。

一度に複数のkey-valueを登録したい

一度に複数のkey-valueを登録できるように、引数にMapを渡せるputメソッドを作成しましょう。

GroovyではLinkedHashMapのリテラルが[key1:value1,key2:value2]といった形で用意されているので、Mapを用いたテストを簡単に書くことができます。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)
        
        then:
        kvs.get(key) == value
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSに登録されているkey-valueを文字列で取得できる" () {

        when:
        kvs.put('aaa','bbb')
        kvs.put('ccc','ddd')
        
        then:
        kvs.dump() == '{aaa:bbb}{ccc:ddd}'
      
    }

+   def "KVSに複数のkeyvalueペアを一度に登録できる" () {
+
+       when:
+       kvs.put(['aaa':'bbb','ccc':'ddd'])
+
+       then:
+       kvs.get(key) == value
+
+       where:
+       key   | value
+       'aaa' | 'bbb'
+       'ccc' | 'ddd'
+   }
}

実行してみましょう。

groovy.lang.MissingMethodException: No signature of method: KeyValueStore.put() is applicable for argument types: (java.util.LinkedHashMap) values: [[aaa:bbb, ccc:ddd]]
Possible solutions: putAt(java.lang.String, java.lang.Object), findAll(), put(java.lang.String, java.lang.String), getAt(java.lang.String), findAll(groovy.lang.Closure)
  at KeyValueStoreSpec.KVSに複数のkeyvalueペアを一度に登録できる(KeyValueStoreSpec.groovy:123)

Process finished with exit code -1

Mapを引数にとるputが存在しないと言われています。予想通りですね。

put(Map)の実装

今回もMap.entrySetとfor文を使えば実装できそうですね。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }

    public void delete(String key) {
        store.remove(key);
    }

    private String debugPrint(String key, String value) {
        StringBuilder result = new StringBuilder();
        result.append("{");
        result.append(key);
        result.append(":");
        result.append(value);
        result.append("}");
        return result.toString();
    }

    public String dump() {
        StringBuilder result = new StringBuilder();
        for(Map.Entry<String,String> pair : store.entrySet()) {
            String keyvalue = debugPrint(pair.getKey(), pair.getValue());
            result.append(keyvalue);
        }
        return result.toString();
    }

+   public void put(Map<String,String> kv) {
+       for(Map.Entry<String,String> pair : kv.entrySet()) {
+           put(pair.getKey(),pair.getValue());
+       }
+   }
}

実行してみましょう。

Process finished with exit code 0

複数登録する際のkeyに一つでもtextが存在する場合は一切登録せず例外発生

空のkvsに対して2番目以降のkeyにtextを指定し、例外が投げられかつdump結果が空文字列ならば仕様を満たしそうですね。 これをテストに落としていきましょう。

Mapリテラルのkeyやvalueにそのまま変数やtextを記述するとシンボル扱いされてしまうので、変数やtextは丸括弧で括るように気をつけてください。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
 
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

    def "KVSに登録されているkey-valueを文字列で取得できる" () {

        when:
        kvs.put('aaa','bbb')
        kvs.put('ccc','ddd')

        then:
        kvs.dump() == '{aaa:bbb}{ccc:ddd}'
    }

    def "KVSに複数のkeyvalueペアを一度に登録できる" () {

        when:
        kvs.put(['aaa':'bbb','ccc':'ddd'])

        then:
        kvs.get(key) == value
        
        where:
        key   | value
        'aaa' | 'bbb'
        'ccc' | 'ddd'
    }

+   def "複数keyvalueペアのkeyに一つでもnullが含まれていたらKVSを更新せず例外を発生させる" () {
+
+       when:
+       kvs.put(['aaa':'bbb',(null):'ddd'])
+
+       then:
+       thrown(IllegalArgumentException)
+       kvs.dump() == ''
+   }
}

実行してみましょう。

Condition not satisfied:

kvs.dump() == ''
|   |      |
|   |      false
|   |      9 differences (0% similarity)
|   |      ({aaa:bbb})
|   |      (---------)
|   {aaa:bbb}
KeyValueStore@5dc598b

  at KeyValueStoreSpec.複数keyvalueペアのkeyに一つでもnullが含まれていたらKVSを更新せず例外を発生させる(KeyValueStoreSpec.groovy:140)

Process finished with exit code -1

例外は発生しましたが、最初のkey-valueがkvsに登録されてしまっています。 どうやら、put(Map)内で使用しているput(String,String)メソッドが投げた例外によってthrown部分のテストが成功したようですね。

登録する前に例外を投げるようにする

各key-valueをkvsへ登録する前に、渡されたMapデータの中身を確認する必要がありそうですね。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

public class KeyValueStore {

    private final Map<String,String> store = new LinkedHashMap<>();

    public void put(String key, String value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

    public String get(String key) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }

    public void delete(String key) {
        store.remove(key);
    }

    private String debugPrint(String key, String value) {
        StringBuilder result = new StringBuilder();
        result.append("{");
        result.append(key);
        result.append(":");
        result.append(value);
        result.append("}");
        return result.toString();
    }

    public String dump() {
        StringBuilder result = new StringBuilder();
        for(Map.Entry<String,String> pair : store.entrySet()) {
            String keyvalue = debugPrint(pair.getKey(), pair.getValue());
            result.append(keyvalue);
        }
        return result.toString();
    }

    public void put(Map<String,String> kv) {

+       for(Map.Entry<String,String> pair : kv.entrySet()) {
+           if(pair.getKey() == null) throw new IllegalArgumentException("keyにnullが含まれています");
+       }

        for(Map.Entry<String,String> pair : kv.entrySet()) {
            put(pair.getKey(),pair.getValue());
        }
    }
}

実行してみましょう。

Process finished with exit code 0

keyとvalueを任意の型で扱えるようにしたい

最後に、Genericを使ってkeyやvalueで任意の型を扱えるようにしましょう。 今回はいきなり製品コードを変更していきます。

package xutp;

import java.util.Map;
import java.util.LinkedHashMap;

-public class KeyValueStore {
+public class KeyValueStore <K,V> {

-    private final Map<String,String> store = new LinkedHashMap<>();
+    private final Map<K,V> store = new LinkedHashMap<>();

-    public void put(String key, String value) {
+    public void put(K key, V value) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        store.put(key,value);
    }

-    public String get(String key) {
+    public V get(K key) {
        if(key == null) throw new IllegalArgumentException("keyがnullでした");
        return store.get(key);
    }

-    public void delete(String key) {
+    public void delete(K key) {
        store.remove(key);
    }

-   private String debugPrint(String key, String value) {
+   private String debugPrint(K key, V value) {
        StringBuilder result = new StringBuilder();
        result.append("{");
-       result.append(key);
+       result.append(key.toString());
        result.append(":");
-       result.append(value);
+       result.append(value.toString());
        result.append("}");
        return result.toString();
    }

    public String dump() {
        StringBuilder result = new StringBuilder();
-       for(Map.Entry<String,String> pair : store.entrySet()) {
+       for(Map.Entry<K,V> pair : store.entrySet()) {
            String keyvalue = debugPrint(pair.getKey(), pair.getValue());
            result.append(keyvalue);
        }
        return result.toString();
    }

-   public void put(Map<String,String> kv) {
+   public void put(Map<K,V> kv) {

-       for(Map.Entry<String,String> pair : kv.entrySet()) {
+       for(Map.Entry<K,V> pair : kv.entrySet()) {
            if(pair.getKey() == null) throw new IllegalArgumentException("keyにnullが含まれています");
        }

-       for(Map.Entry<String,String> pair : kv.entrySet()) {
+       for(Map.Entry<K,V> pair : kv.entrySet()) {
            put(pair.getKey(),pair.getValue());
        }
    }
}

実行してみましょう。

Process finished with exit code 0

既存のテストを一切壊さずに変更ができました。 Groovyは動的型付けな言語なので、先ほどのような型の変更の際にも柔軟に対応可能です。

他の型でもテストしてみよう

試しに、いくつかのparameterized testのデータを書き換えてみましょう。

package xutp

import spock.lang.*

class KeyValueStoreSpec extends Specification {

    def kvs

    def setup() {
        kvs = new KeyValueStore()
    }

    def "空のKVSにkeyとvalueのペアをputするとkeyに対応するvalueがgetできる" () {

        when:
        kvs.put(key,value)

        then:
        kvs.get(key) == value

        where:
        key   | value
        'aaa' | 'bbb'
-       'ccc' | 'ddd'
+       1     | 2.5
    }

    def "KVSにnullのkeyをputすると例外が発生する" () {

        when:
        kvs.put(null,'null')

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに対してnullでgetを行うと例外が発生する" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.get(null)

        then:
        thrown(IllegalArgumentException)
    }

    def "KVSに登録されていないkeyでgetを行うとnullが返る" () {

        when:
        kvs.put('aaa','bbb')

        then:
        kvs.get('other') == null
    }

    def "既にKVSに存在するkeyをputするとvalueのみ更新される" () {

        given:
        kvs.put('aaa','bbb')

        when:
        kvs.put('aaa','ccc')

        then:
        kvs.get('aaa') == 'ccc'
    }

    def "既にKVSに存在するkeyについてdeleteを行うとkeyとvalueのペアがKVSから削除される" () {

        given:
        kvs.put(key,value)

        when:
        kvs.delete(key)

        then:
        kvs.get(key) == null
 
        where:
        key   | value
        'aaa' | 'bbb'
-       'ccc' | 'ddd'
+       1     | 2.5
    }

    def "KVSに登録されているkey-valueを文字列で取得できる" () {

        when:
        kvs.put('aaa','bbb')
        kvs.put('ccc','ddd')

        then:
        kvs.dump() == '{aaa:bbb}{ccc:ddd}'
    }

    def "KVSに複数のkeyvalueペアを一度に登録できる" () {

        when:
-       kvs.put([('aaa'):('bbb'),('ccc'):('ddd')])
+       kvs.put(map)

        then:
        kvs.get(key) == value

        where:
-       ket   | value
-       'aaa' | 'bbb'
-       'ccc' | 'ddd'
+       map                       | key   | value 
+       ['aaa':'bbb','ccc':'ddd'] | 'aaa' | 'bbb'
+       ['aaa':'bbb','ccc':'ddd'] | 'ccc' | 'ddd'
+       [1:2.5,10:30.0]           | 1     | 2.5
+       [1:2.5,10:30.0]           | 10    | 30.0
    }

    def "複数keyvalueペアのkeyに一つでもnullが含まれていたらKVSを更新せず例外を発生させる" () {
 
        when:
        kvs.put(['aaa':'bbb',(null):'ddd'])
 
        then:
        thrown(IllegalArgumentException)
        kvs.dump() == ''
    }
}

実行してみましょう。

Process finished with exit code 0

問題なく動作しているようですね。

序、完

序では、テストコードと製品コードを同一実行環境上で動作する別言語で書いてみました。いかがでしたでしょうか? GroovyはJavaに近いコードが書けるので、それほど違和感なく読むことが可能だったのではないでしょうか。

それでは、次回はTDD 破です。