TDD Boot Camp 名古屋 1 日目でやったことを Python で復習する その 2
今日は TDD Boot Camp 名古屋 1 日目のペアプロ体験の後半でやった仕様変更の復習をするよ!
前回のものに仕様変更を加えていく.
仕様変更 1
一度にいっぱい set
できる set_multi
と一度にいっぱい get
できる get_multi
を実装する仕様変更.
正確仕様はちょっとうろ覚えだけど,だいたいこんな感じの仕様.
>>> store = FileStore() >>> store.set_multi({'foo': 'hoge', '': 'toto', 'bar': 'fuga'}) >>> store.dump() 'foo:hoge\nbar:'fuga\n' >>> store.get_multi(['bar', 'foo', None]) ['fuga', 'foo']
まず set_multi
のテストから
class TestFileStore(unittest.TestCase): def setUp(self): self.store = FileStore() self.store.set('foo', 'hoge') def test_set_multi(self): self.assertEqual(self.sotre.dump(), 'foo:hoge\nbar:fuga\nbuzz:piyo\n')
assert から書くのを忘れない(*'ω')b
次に,この assert が成功する用に set_multi
する.
class TestFileStore(unittest.TestCase): def setUp(self): self.store = FileStore() self.store.set('foo', 'hoge') def test_set_multi(self): self.store.set_multi({'bar': 'fuga', 'buzz':'piyo'}) self.assertEqual(self.store.dump(), 'foo:hoge\nbar:fuga\nbuzz:piyo\n')
ここまでできたら set_multi
を実装する.
set_multi
の実装
テストができたから set_multi
を実装する.
明白な実装なのでそのまま実装しちゃう.
実装はこんな感じ
class FileStore(object): def __init__(self): self._store = OrderedDict() def set(self, key, value): if key is None or key is '': return if key in self._store: self._store.pop(key) self._store[key] = value def set_multi(self, pairs): for k, v in pairs.items(): self.set(k, v) ...
テストもばっちり成功!!
get_multi
のテストと実装
set_multi
と同様に get_multi
も行います.
1. テスト作成
2. テスト失敗
3. 実装
4. テスト成功
は変わりません.
うんで,出来上がるテストと実装はこんな感じ.
class FileStore(object): ... def get(self, key): return self._store.get(key, None) def get_multi(self, keys): return [self.get(k) for k in keys if k in self._store] ... import unittest class TestFileStore(unittest.TestCase): def setUp(self): self.store = FileStore() self.store.set('foo', 'hoge') ... def test_get_multi(self): self.store.set_multi({'bar': 'fuga', 'buzz':'piyo'}) self.assertEqual( self.store.get_multi(['buzz', 'toto', '', 'bar']), ['piyo', 'fuga'])
set_multi
も get_multi
も既存のメソッドに影響を与えないので,わりと簡単に追加できた.
次の仕様変更はちょっと大変だよ!
仕様変更 2
メインディッシュきた!
次の仕様変更はこんな感じ.
>>> store = FileStore() >>> store.set('foo', '${now}') # 現在時刻が登録される >>> store.get('foo') 2010-07-16T21:57:48
テストがかけない.
残念ながらこのままじゃテストを書けない (>_<)
現在時刻とかをテストで扱うのはぶっちゃけ無理.
テストが書けない場合,テストができない原因になっているところだけ取り出して,それ以外のところでテストすればいいと思う.
なので開き直って現在時刻をテストするのはしない.
ここではテストするのは set が "${now}" を何かしら展開しよするかどうかをテストする.
そんなテストはこんな感じに書いた.
class FakeExpander(object): def expand(self, value): if value == '${now}': return '%s is expanded' % value return value class TestFileStore(unittest.TestCase): def setUp(self): self.store = FileStore() self.store.set('foo', 'hoge') def test_set(self): fake_expander = FakeExpander() self.store.set_expander(self.fake_expander) self.store.set('bar', '${now}') self.assertEqual(self.store.dump(), 'foo:hoge\nbar:${now} is expanded\n') ...
このテストがクリアされるように set
を修正し,こっそり増えた set_expander
を追加する.
class Expander(object): def expand(self, value): if value == '${now}': return datetime.now().strftime('%Y-%m-%d %H:%M:%S') return value class FileStore(object): def __init__(self): self._store = OrderedDict() self._expander = Expander() def set_expander(self, expander): self._expander = expander def expand(self, value): return self._expander.expand(value) def set(self, key, value): if key is None or key is '': return if key in self._store: self._store.pop(key) self._store[key] = self.expand(value) ...
さらにこっそり,expand
も追加されてるけど気にしない.
というのも set_expander
にしろ,expand
にしろそこに不安はないのでテストはない.
もう一点,Expander
っていうクラスを追加した.
ここがまさにテストの出来ない部分で,ここについてのテストは書かないことにした.
テストが出来ない部分はテストが必要ないくらい単純なものにしてやれば,開き直ってテストなしでいいと思う.
さらに Expander
クラスを導入したことで,"${now}" 以外に "${name}" とか増やしたくなった場合でも,FileStore にテストを追加する必要はなくて,Expander の方をごにょごにょしてやればよくなってます.
まとめ
TDD Boot Camp 名古屋の 1 日目でやったことの復習をした.
とりあえず,仕様変更 2 までやった.
仕様変更 3 (データの生存期間指定機能の追加) は力尽きたので省略しちゃう (ごめんなさい (>_<) )
仕様変更 3 も仕様変更 2 のときと同じように,テストができない部分は最小化してモックオブジェクトとか作ってテストすればいいと思う.
仕様変更 3 の場合だったら,CachedFileStore
クラスを導入してデータの保持とかは FileStore
に委譲.
CachedFileStore
はデータの生存,消滅の管理やらせて,消滅させるかどうかの判断だけを行うクラスを導入.
仕様変更 3 でテストできないのが時間関連の判断だけなので,そこをちっちゃいクラスに閉じ込めてしまえば他の部分は全部テストできるようになるっていう思惑.
責任の分割,超大切.
TDD では,とにかくテストから書く.
テストの中では assert から書く.
普通のプログラムはメソッドの上から書くけど,テスト プログラムはメソッドの下から書く.
テストもメンテナンスする.
最期に最終的なコードを.
#!/usr/bin/env python3 # -*- utf-8 -*- from datetime import datetime from collections import OrderedDict class Expander(object): def expand(self, value): if value == '${now}': return datetime.now().strftime('%Y-%m-%d %H:%M:%S') return value class FileStore(object): def __init__(self): self._store = OrderedDict() self._expander = Expander() def set_expander(self, expander): self._expander = expander def expand(self, value): return self._expander.expand(value) def set(self, key, value): if key is None or key is '': return if key in self._store: self._store.pop(key) self._store[key] = self.expand(value) def set_multi(self, pairs): for k, v in pairs.items(): self.set(k, v) def get(self, key): return self._store.get(key, None) def get_multi(self, keys): return [self.get(k) for k in keys if k in self._store] def dump(self): return '\n'.join( [':'.join([k, v]) for k, v in self._store.items()]) + '\n' import unittest class FakeExpander(object): def expand(self, value): if value == '${now}': return '%s is expanded' % value return value class TestFileStore(unittest.TestCase): def setUp(self): self.store = FileStore() self.store.set('foo', 'hoge') def test_set(self): fake_expander = FakeExpander() self.store.set_expander(fake_expander) self.store.set('bar', '${now}') self.assertEqual(self.store.dump(), 'foo:hoge\nbar:${now} is expanded\n') def test_get1(self): self.assertEqual(self.store.get('foo'), 'hoge') def test_get_miss_hit(self): self.assertEqual(self.store.get('toto'), None) def test_set_invalid_key1(self): self.store.set('bar', 'fuga') self.store.set(None, 'momo') self.assertEqual(self.store.dump(), 'foo:hoge\nbar:fuga\n') def test_set_invalid_key2(self): self.store.set('bar', 'fuga') self.store.set('', 'momo') self.assertEqual(self.store.dump(), 'foo:hoge\nbar:fuga\n') def test_set_overwrite(self): self.store.set('bar', 'fuga') self.store.set('foo', 'buzz') self.assertEqual(self.store.dump(), 'bar:fuga\nfoo:buzz\n') def test_dump1(self): self.assertEqual(self.store.dump(), 'foo:hoge\n') def test_dump3(self): self.store.set('bar', 'fuga') self.assertEqual(self.store.dump(), 'foo:hoge\nbar:fuga\n') def test_set_multi(self): self.store.set_multi({'bar': 'fuga', 'buzz':'piyo'}) self.assertEqual(self.store.dump(), 'foo:hoge\nbar:fuga\nbuzz:piyo\n') def test_get_multi(self): self.store.set_multi({'bar': 'fuga', 'buzz':'piyo'}) self.assertEqual( self.store.get_multi(['buzz', 'toto', '', 'bar']), ['piyo', 'fuga']) if __name__ == '__main__': unittest.main()