ペアプロでTDDやってみよう(ライフゲーム) その1

アジャイルなソフトウェア開発を導入する手伝いという仕事の中で、ペアプロでTDDをやってみせるという機会がよくあります。ここ最近のバディであるid:haru01と組んで、ライフゲームをやってみせるのが定番化しています。

ライフゲームをテーマにしてどんなふうにTDDを進めるのか、これから簡単に紹介していきたいと思います。ベーシックな手法のみで進めるので、モック(テストダブル)とか、データドリブンテストとかそういうことはしません。言語はPythonですが、誰にでも読みやすいのがPythonのいいところなので、Pythonを知らなくても理解できるんじゃないかと思います。

ライフゲームにはWikipediaにもあるように、誕生・生存・過疎・過密の4つのルールがあります。TDDでライフゲームを書くには、この4つをもとにTODOリストを作ります。

  1. 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
  2. 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
  3. 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
  4. 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

このTODOリストをもとにテストを書き始めます。最初の一歩は、とにかくRedです。

life_test.py:

# coding: utf8

import unittest

class GameOfLifeTest(unittest.TestCase):
    def test(self):
        self.fail()

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

これで、テストケースの書き方が間違ってないか確認したら、TODOリストの1番をテストで書いてみます。しかし書こうと思うと、すぐにいくつかの問題に直面します。

  • 具体的な例(ここでは「死んでいるセルに隣接する生きたセルがちょうど3つある」という状態)はどんなパターンにするか
  • 現在の状態をどう渡せばいいのか
  • 次の世代に移行する方法
  • 「誕生する」をどうやって確認するか

いっぺんに解くには問題がややこしいので、いったん分離したいと思います。まずはパターンをプログラムとしてどう表現して渡すのか、ここに着目しましょう。TODOリストに項目を追加します。

  1. 初期パターンを渡せる。 <- 追加
  2. 死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代が誕生する。
  3. 生きているセルに隣接する生きたセルが2つか3つならば、次の世代でも生存する。
  4. 生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
  5. 生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。

具体例はWikipediaにある3x3のパターンを使うことにします。プログラム上のデータ構造としてはどう作ればいいでしょう。現時点では、実装の内部構造に立ち入りたくないので、データ構造に依存しない形で、かつテストケースがわかりやすくなるようにしておきたい。今回は、初期パターンを文字列で渡すことにします。アサートも、現在のパターンを文字列で取り出して比較しましょう。こんなテストケースを書きました。

life_test.py:

    def test_set_initial_pattern(self):
        initial = '''
oo.
o..
...
'''
        game = GameOfLife(pattern=initial)

        expected = '''
oo.
o..
...
'''
        actual = game.dump()
        self.assertEqual(actual, expected)

生きているセルを「o」、死んでいるセルを「.」で表現しました。Pythonでは複数行文字列を「'''」(または「"""」)で囲んで表現します。インデントのせいでちょっとだけわかりにくくなっています。「self」はJavaなどのthisと同じで、Pythonでは常に明示し、メソッドの第1引数に指定する必要があります。

次に最低限Greenになる実装をします。Fake Itです。

life.py

# coding: utf8

class GameOfLife(object):
    def __init__(self, pattern):
        self.pattern = pattern

    def dump(self):
        return self.pattern

「def __init__(...)」はコンストラクタの定義です。GameOfLifeクラスのインスタンスを作るには、GameOfLife()と括弧付きで呼び出します。このタイミングで__init__が呼び出されることになっています。

書いたコードではコンストラクタで受け取ったものをdump()で返すだけなので、明らかに実装が足りませんが、TODOリストの最初の項目に限定すればこれで十分でした。もうちょっと書きたかったのですが、続きは次のTODOに持ち越すことにします。

TODOリストの次の項目は、さっき着手しかけた誕生のルールです。初期パターンの設定と現パターンの取得はすでにあるので、次の世代に移行する方法さえ決まればテストを書けます。時間を進めるという意味でtickというメソッドにしましょう。

life_test.py:

    def test_born(self):
        initial = '''
oo.
o..
...
'''
        game = GameOfLife(pattern=initial)
        game.tick()

        expected = '''
oo.
oo.
...
'''
        actual = game.dump()
        self.assertEqual(actual, expected)

一見するとさきほどのテストと変わりませんが、game.tick()の行が増えているのと、expectedのパターンがちょっとだけ変化しています。中央のセルが生きている状態になるわけです。

テストはこれで良さそうなのですが、ちょっと気になる点もあります。いまテストしたいのは「中央のセルが誕生する」ことだけなのに、いまのアサートでは3x3すべてのセルを確認しています。左上のセルが生存したり、右下のセルが死滅状態のまま変化しないことまで確認していることになります。テストの意図を明らかにするには、以下のようなテストのほうがいい気もします。

life_test.py:

    def test_born(self):
        initial = '''
oo.
o..
...
'''
        game = GameOfLife(pattern=initial)
        game.tick()

        self.assertTrue(game.is_alive(1, 1))

is_alive(x, y)で座標を指定して生きているかどうか調べるという方法です。これはこれで、面倒な気もしますね。(1, 1)という座標指定も、たとえば10x10のグリッドを想像すると到底直感的ではなさそうです。今回は前者のやりかたで進めることにします。

誕生のテストが書けたので、誕生のロジックを考えましょう。

というところで、長くなってきたので続きはこちらです。途中のコードは https://github.com/yattom/tdd-life で参照できます。