序として選んだのは「テストコードをGroovyで、製品コードをJavaで書く」というものです。
テスティングフレームワークにはSpockを使用します。また、実行環境にはJava SE 7、Groovy 1.8.5を用います。
まずは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クラスがないと怒られました。確かに、まだ作っていないですね。作りましょう。
+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を作りましょう。
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メソッドはないよと言われました。これも作りましょう。
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が返ってきました。では、テストに通るように実装しましょう。
テストコードのテストをやっておきたいので、仮実装でテストが通るかどうか確認します。
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だと例外が発生してほしいようです。まずはテストを書きます。
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
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
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
この仕様に関しては、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
予想通り、テストが成功しました。
これも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
ここからは、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がないのでエラーが発生しています。実装してしまいましょう。
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
kvsに登録されているkeyとvalueの一覧を文字列で返す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メソッドは存在しないことがわかります。
簡単そうなので一気に実装してしまいましょう。
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のテストにつけていた@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のテストに失敗した状態まで戻りました。
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にメソッドのインライン化を適用すべきかどうかが悩ましいところです。
現状、いつフォーマットが変更されるかわからないのでこのままにしておきますが、フォーマットが変更されないと判明した時点でdumpメソッドに展開してしまいましょう。
一度に複数の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が存在しないと言われています。予想通りですね。
今回も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
空の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
最後に、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 破です。