TDD Boot Camp 名古屋 1 日目でやったことを Python で復習する

タイトルとおりです.

id:t-wada さんからも許可得ることができたのでお題も Python で書き直してみます.


あ,ちなみに Python 3.1 です.

お題

↓のような仕様の FileStore クラスを作ります.

>>> store = FileStore()
>>> sotre.set('foo', 'hoge')
>>> store.get('foo')
'hoge'
>>> store.dump()
'foo:hoge\n'
>>> store.set('bar', 'fuga')
>>> store.dump()
'foo:hoge\nbar:fuga\n'
>>> store.get('toto')
None
>>> store.set(None, 'momo')
>>> store.dump()
'foo:hoge\nbar:fuga\n'
>>> store.set('foo', 'piyo');
>>> store.dump()
'bar:fuga\nfoo:piyo\n'

まずはテストから書く

get のテスト書く.

assert first

テストを書くときは assert から書く.

import unittest

class TestFileStore(unittest.TestCase):

    def test_get1(self):
        self.assertEqual(store.get('foo'), 'hoge')


if __name__ == '__main__':
    unittest.main()

こんな感じ.

でもこれを実行するとエラーになっちゃう.

% FileStore.py
E
======================================================================
ERROR: test_get1 (__main__.TestFileStore)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "FileStore.py", line 10, in test_get1
    self.assertEqual(store.get('foo'), 'hoge')
NameError: global name 'store' is not defined

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

とりあえずエラーにならないところまで書く.

それと,そもそも set して get すると値が得られるという仕様のはずなので set も追加する.

#!/usr/bin/env python3
# -*- utf-8 -*-

class FileStore(object):

    def set(self, key, value):
        pass

    def get(self, key):
        pass


import unittest

class TestFileStore(unittest.TestCase):

    def test_get1(self):
        store = FileStore()
        store.set('foo', 'hoge')
        self.assertEqual(store.get('foo'), 'hoge')


if __name__ == '__main__':
    unittest.main()

これで Error じゃなくて Fail になったはず.

% FileStore.py
F
======================================================================
FAIL: test_get1 (__main__.TestFileStore)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "FileStore.py", line 21, in test_get1
    self.assertEqual(store.get('foo'), 'hoge')
AssertionError: None != 'hoge'

----------------------------------------------------------------------
Ran 1 test in 0.000s

よし,おk.

仮実装 (Fake it)

Fake itはテストのテスト

TDD Boot Camp 名古屋に登壇させていただきました - t-wadaの日記

ということでまずgetを仮実装する.

class FileStore(object):

    def set(self, key, value):
        pass

    def get(self, key):
        return 'hoge'

そしてテスト.

% FileStore.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

やったー.「OK」だってーーー!!!

三角測量 (Triangulation)

さぁ,仮実装をいじめるぞ!!

class TestFileStore(unittest.TestCase):

    def test_get1(self):
        store = FileStore()
        store.set('foo', 'hoge')
        self.assertEqual(store.get('foo'), 'hoge')

    def test_get2(self):
        store = FileStore()
        store.set('bar', 'fuga')
        self.assertEqual(store.get('bar'), 'fuga')

ぎゃ!仮実装じゃこんなテストを追加されたらひとたまりもない.

ということでset, getを実装します.

#!/usr/bin/env python3
# -*- utf-8 -*-

from collections import OrderedDict

class FileStore(object):

    def __init__(self):
        self._store = OrderedDict()

    def set(self, key, value):
        self._store[key] = value
    
    def get(self, key):
        return self._store[key]


import unittest

class TestFileStore(unittest.TestCase):

    def test_get1(self):
        store = FileStore()
        store.set('foo', 'hoge')
        self.assertEqual(store.get('foo'), 'hoge')

    def test_get2(self):
        store = FileStore()
        store.set('bar', 'fuga')
        self.assertEqual(store.get('bar'), 'fuga')
        
if __name__ == '__main__':
    unittest.main()
% FileStore.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

(*'ω')b

ここでは,dump で順番の保持が必要そうなので collections.OrderedDict 使ったよ.

テスト->実装->リファクタリング

この調子で dump とか get,set の細かい仕様を実装していく

まずdump
class FileStore(object):

    ...

    def dump(self):
        return '\n'.join(
            [':'.join([k, v]) for k, v in self._store.items()]) + '\n'


import unittest

class TestFileStore(unittest.TestCase):

    def test_get1(self):
        store = FileStore()
        store.set('foo', 'hoge')
        self.assertEqual(store.get('foo'), 'hoge')
        
    def test_get2(self):
        store = FileStore()
        store.set('bar', 'fuga')
        self.assertEqual(store.get('bar'), 'fuga')

    def test_dump1(self):
        store = FileStore()
        store.set('foo', 'hoge')
        self.assertEqual(store.dump(), 'foo:hoge\n')

    def test_dump2(self):
        store = FileStore()
        store.set('bar', 'fuga')
        self.assertEqual(store.dump(), 'bar:fuga\n')

    def test_dump3(self):
        store = FileStore()
        store.set('foo', 'hoge')
        store.set('bar', 'fuga')
        self.assertEqual(store.dump(), 'foo:hoge\nbar:fuga\n')
  1. テスト test_dump1 を作成
  2. テスト実行: 失敗
  3. FileStore.dump を仮実装
  4. テスト実行: 成功
  5. テスト test_dump2 を作成
  6. テスト実行: 失敗
  7. FileStore.dump を実装
  8. テスト実行: 成功
  9. テスト test_dump3 を作成
  10. テスト実行: 成功

っていう流れ.

テストのリファクタリング

いい加減 store = FileStore() とか store.set('foo', 'hoge') って書くのがだるいのでテストをリファクタリングする.ついでに test_get2, test_dump2 ももういらないので削除する.

で,こうなる

class TestFileStore(unittest.TestCase):

    def setUp(self):
        self.store = FileStore()
        self.store.set('foo', 'hoge')

    def test_get1(self):
        self.assertEqual(self.store.get('foo'), 'hoge')
        
    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')
細かい仕様を実装

とりあえずは最終形.

#!/usr/bin/env python3
# -*- utf-8 -*-

from collections import OrderedDict

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 get(self, key):
        return self._store.get(key, None)

    def dump(self):
        return '\n'.join(
            [':'.join([k, v]) for k, v in self._store.items()]) + '\n'


import unittest

class TestFileStore(unittest.TestCase):

    def setUp(self):
        self.store = FileStore()
        self.store.set('foo', 'hoge')

    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')
        

if __name__ == '__main__':
    unittest.main()

やっぱり,ここでも

  1. テスト作成
  2. テスト実行: 失敗
  3. 実装
  4. テスト実行: 成功

の流れには逆らわずにひたすらまわす.

とりあえず

基本仕様の実装ができました.

明日はこれに,TDD Boot Camp でもやった仕様変更を追加していきたい.


今日は眠いのでおしまい!!