導入
読んだので 前回 に引き続き振り返ってみようと思います。
概要
第 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)
一回目の print
で False
かそれと等価な None
や 0
が出力されて、二回目の print
で True
やそれと等価な 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行目が通り、最後まで通せます。
が、当然コンストラクタで wasRun
に None
をセットしてから何も更新していないので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 は全部潰している訳ではないので、宿題として楽しむことができます。
これただの本の要約になってるのでこの辺にしておきます。 気になる人は買って読んでください。