魂の生命の領域

AWS とか Python とか本読んだ感想とか哲学とか書きます

テスト駆動開発の第 II 部 xUnit 読んだ

導入

読んだので 前回 に引き続き振り返ってみようと思います。

概要

第 I 部は「2ドルと3フランを(適切な換算レートで)足し算する」ようなオブジェクトをテスト駆動で書いていこう、というお話でした。言語としては Java を使っていたのでそれを私は普段使っている Python で書き直していく流れで取り組みました。

第 II 部はユニットテストのライブラリをテスト駆動で作る、というイカした内容になっています。 また、第 I 部とは違い初めから Python で書かれています。 なので普通に写経しただけっていう感じです。

第 I 部を読んでいるときは Python の標準ライブラリに含まれる unittest を使ってテストを書きました。 が、ここではその unittest に相当するものをテスト駆動で作っていく感じです。

テストのテストっていきなり言われても全然イメージつかないですね。

本題

なぜか 前回 で触れていなかったのですが、本書では TODO リストを作ってそれを順次消化したり追加したりしながら方向性を管理しています。 この第 II 部 xUnit で最初に挙げられる TODO はこちらです。

  • テストメソッドを呼び出す
  • setUp を最初に呼び出す
  • tearDown を後で呼び出す
  • テストメソッドが失敗したとしても tearDown を呼び出す
  • 複数のテストを走らせる
  • 収集したテスト結果を出力する

単体テストのライブラリを知っていれば「そりゃそうだ」となるような項目ですね。 でもこうやって TODO を挙げたのであとはこれらをチェックするようなテストを書いていけばいいだけとなります。 TODO 書いて管理するのはいつの世も強い。 テストツールをテストするコードを書くために若干混乱しがちですが、上に書いた TODO はこれから実装するテストツールが持つべき仕様として挙げたものであり、これらの機能をテストするコードを書いていくことになります。

基本的な戦略としては wasRun というメソッドをテストが実行されたかどうかのフラグとし、こいつにテストメソッドの名前を渡して、テスト実行前後のこのフラグを見ればいいというふうになります。

なのでテストとしては以下のようになります。(引用)

test = WasRun('testMethod')
print(test.wasRun)
test.testMethod()
print(test.wasRun)

一回目の printFalse かそれと等価な None0 が出力されて、二回目の printTrue やそれと等価な 1 などが出力されれば OK ということですね。

これのテスト対象となるコードとして、インスタンス生成時に引数をとりあえず取れるようにすると、こんな感じに書けます。

class WasRun:
    def __init__(self, name):
        pass

実行すると1行目は通りますが、2行目の test.wasRun でエラーになります。 これで test.wasRun として None がセットされるので2行目と4行目が通ります。

class WasRun:
    def __init__(self, name):
        self.wasRun = None

3行目も同じように test.testMethod() があるのでエラーにならないようにこのメソッドを定義します。

class WasRun:
    def __init__(self, name):
        self.wasRun = None

    def testMethod(self):
        pass

これで3行目が通り、最後まで通せます。 が、当然コンストラクタで wasRunNone をセットしてから何も更新していないので2行目と4行目はどちらも None が出力されます。 一番安直な解決策は testMethod() が呼ばれたときに wasRun を1にすることです。

class WasRun:
    def __init__(self, name):
        self.wasRun = None

    def testMethod(self):
        self.wasRun = 1

これで最初に書いたテストコードが全部想定通りに動いたことになります。

さらに、テストメソッドを直接呼び出すよりインタフェースを噛ませてそれを呼び出せば OK なようにしてやるのがよりベターです。 なので run メソッドを定義してこれを呼び出せばテストメソッドを呼び出せるようにします。 まずは直接テストメソッドを呼び出している箇所を書き換えます。

test.testMethod()

こうします。

test.run()

テスト対象のコードは、ここが

def testMethod(self):
    self.wasRun = 1

こうなります。

def run(self):
    self.testMethod()

def testMethod(self):
    self.wasRun = 1

ここから、より一般的な形に拡張していきます。

class WasRun:
    def __init__(self, name):
        self.wasRun = None
        self.name = name

    def run(self):
        method = getattr(self, self.name)
        method()

    def testMethod(self):
        self.wasRun = 1

こんな感じです。 getattr は組み込み関数で、第一引数にオブジェクト、第二引数にオブジェクトの持つ属性を文字列で渡すことでその属性の値を取得できます。 これでテストメソッドの名前を動的に指定できるようになります。

ちなみに getattr をこれまでまともに使ってこなかったので、検証用に書いたコードがこちらです。

class Ore:
    def __init__(self, name):
        self.name = name

    def hoge(self):
        method = getattr(self, self.name)
        method()

    def greet(self):
        print('aaa')


ore = Ore('greet')
ore.hoge()

俺クラスのインスタンスOre('greet') で宣言します。すると name という属性に 'greet' が入ります。 ここで、 hoge メソッドを呼び出すと、まず getattr によって self つまり自分自身の name という属性が取り出されて method として定義されます。そして method() が実行されるので ore.hoge() の結果として標準出力に aaa が出力されます。 俺クラスの挨拶は実際「あ、あ…」なのでまぁこんな感じでしょう。

で、こうなるとテストメソッドの呼び出し自体は別のクラスに切り出して、 WasRun クラスの仕事はテストが実行されたかのチェックだけにした方が良さそうです。 本書内ではテストメソッドの呼び出しを行う TestCase という親クラスを定義しこれを WasRun クラスはそれを継承する子クラスとしていきます。

こんな感じで TODO を処理していきながらリファクタリングも繰り返し、どんどん作っていきます。 拡張していくにつれて、上で引用したような書き方だと一般性に欠ける書き方がどんどん出てきてしまうのでそれを随時最小限のコード改修で解決していきます。

ちなみに上で引用した TODO は全部潰している訳ではないので、宿題として楽しむことができます。

これただの本の要約になってるのでこの辺にしておきます。 気になる人は買って読んでください。