TDDの考え方を身につける

DocTestの有用性と疑問

DocTestの有用性として、私は以下の3つを挙げます。

  1. 実装とテストが分離しない(tests.pyなどに分かれていない)
  2. ドキュメントの内容を動かして確認出来るため実装とヘルプが乖離しない
  3. help()やSphinxと連携できるため後々有用

しかし同時に以下のような使いにくさや疑問も感じました。

  1. UnitTestに比べて評価方法がequalしかないので検査しづらい
  2. 関数単体には使えるのは分かった。クラスとか複雑なものにどこまで 適用できるのか疑問
  3. 実際のところ、脳内にある実装をまず書き起こして、テストはそれを 検証するために書くと思う。

上記の疑問は、UnitTestを書いたことがある人はどこかで感じている事なのでは ないかと思います。そしてこれはここまで説明したDocTestの使い方を覚えても 同じように感じる人も多いと思います。

さきほどまでの例のようなDocTestの使い方は 値の入力とそれに対する期待値を確認 して、回帰バグを防止するために 役立ち、常にコードが正しく動作していることを保証してくれるため、 漸進的な開発をサポートしてくれます。

しかしこれをもって テスト開発駆動 していると言うには ちょっと弱い気がするのも事実です。

TDDは設計手法

TDDはテスト手法、ではなく、 TDDは設計手法 、と定義してみましょう。 この設計手法を使う場合、「脳内に出来上がった実装を確認するために使う」 のではなく、 「脳内に考えていることをそのままテストとして書き出していって、 それを通すために実装する」 というように手順を変えてください。

前述のDocTestの例は、まず関数名があって、そこにdocstringを書いていました。 add の動作を確認するDocTest は自然と「数値だったらどうなるか」とか 「文字列だったらどうなるか」「文字列と数値だったら?」というテストケース をdocstringに書いていくという作業に繋がっていってしまいます。

そのような例を使ってTDDを理解しようとすると、前述のような疑問が出てきて、 テスト手法としては使えても、設計にはなかなか結びつきません。

以降の説明では「DocTestで開発を駆動」させていきます。

注釈

ところで、UnitTest書いてるからテスターは不要、というのも間違いだと 考えています。UnitTestでテスト出来るのは実装者が思い描く実装が 正しく動いていることだけです。

DocTest駆動開発

DocTest駆動開発という手法が固有名詞かどうかは知りませんが、思考を Testからちょっとドキュメントに傾けてみた手法だと思ってください。 きっと略記は DDD ですね。

DocTestで開発を駆動させる考え方は以下の通りです。

  • DocTestはdocstringに書きます。
  • docstringは利用者に説明するためのドキュメントです
  • ゆえに、docstringにはテストではなく使い方の説明を書くのが本筋です
  • であれば、docstringには 自分がどう使いたいか を書きます。

実装出来るか出来ないかどう実装するかは考えずに、どんな機能が必要か (あったら便利か、ではない)、それを使うときコード上でどう使いたいか のみを考えてdocstringを書きましょう。

方眼紙を操作するクラス Hougan を作る

あなたはプログラマーとして、方眼紙を操作するクラス、というか某有名 表計算ソフトのデータを操作するクラスを実装することになりました。

「表計算ソフトと言えばセルが縦横に並んでいる2次元データを扱うのか。 であればリスト型を拡張して……」、と思考を進めるのはちょっとまってください。 それは前述の「実装方法は考えない」という前提に反しています。

まずは利用方法をdocstringに書き下していきましょう。 ハンズオンなので、後でまったく違うイメージのものが出来てしまわないように、 おおまかなdocstringのひな形を以下のように用意しました。

# -*- coding: utf-8 -*-

class Hougan(object):
    """
    Hougan は縦横のマス目のある方眼紙状のデータを簡単に扱うクラスです。

        >>> hougan = Hougan()
        >>> hougan
        ????

    方眼紙には行と列があり、それぞれが0行目、0列目からはじまります。
    方眼紙の各マス目をそれぞれセルとよび、その座標は行と列の
    インデックスで表されます。つまり一番左上は `セル(0, 0)` です。

    セルは、任意の座標(ここでは0,0)を指定してhouganから以下のように
    取り出すことが出来ます。

        >>> cell = ????
        >>> cell
        ????

    各セルには値(value)と背景色(color)の属性があり、値には数値や
    文字列を格納できますが初期値はNoneです。

        >>> cell.value is None
        True

        >>> cell.value = 1

    色は0から7までの値で表現され、それぞれ以下の色を意味しています。

        == ========
        値 色名
        -- --------
        0  black
        1  white
        2  red
        3  green
        4  brue
        5  yellow
        6  magenda
        7  cyan
        == ========

    colorは値で取得・設定します。

        >>> cell.color
        ????

        >>> cell.color = 1


    座標1,1に値を設定するには、例えば以下のようにします。

        >>> ????

    Houganインスタンスは座標の最大値を保持しています。

        >>> ????
        ????

    方眼紙はテキストで出力することができます。テキストの場合、色は
    表示されず、値が方眼紙状に表示されます。

        >>> print ????
        ????

    将来的にはcsv形式や表計算ソフトで読み込める形式の出力も実装
    する予定ですが、現在はサポートしていません。
    """

if __name__ == '__main__':
    import doctest
    doctest.testmod(
        optionflags = (doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) )

上記のdocstring内にある ???? の部分は自分好みの実装方法に置き換えて ください。どのような関数定義にするか考えて見てください。 また、上記のひな形で記載したDocTestはclassのdocstringです。実際にこの テストをパスするためにはメソッドなどを実装する必要がありますが、 各メソッドにもDocTestを書いてください。その際に、classのdocstringには 総合的な使い方を記載する、各メソッドにはメソッドの細かい仕様の説明 を記載するよう意識して行ってください。

実装に移る前にちょっとだけtestmod()の引数ついて説明します。

NORMALIZE_WHITESPACE

NORMALIZE_WHITESPACEは空白の数の違いを無視してくれます。 このオプションを指定しない場合、以下のDocTestは2つ目がエラーとなります:

>>> (1,2,3)
(1, 2, 3)

>>> (1,2,3)
(1,2,3)

これはPythonの対話コンソールで表示される内容と完全一致しないとエラー とするDocTestの仕様ですが、ちょっとした空白文字の数の違いでエラーと なってしまうため使いにくくなってしまいます。他にも改行の数にも 敏感に反応してしまいます。こういった空白無視の指定が NORMALIZE_WHITESPACE オプションです。

ELLIPSIS

ELLIPSISは省略記法 ... を使えるようにします。 このオプションを指定しな場合、以下のDocTestはエラーとなります:

>>> class Foo(object):
...     pass
...
>>> Foo()
<Foo object at 0x0281BFF0>

上記の期待値の例には16進数0x0281BFF0が含まれていますが、これは Fooインスタンスの格納アドレスなので実行毎に別の値になります。 これを省略して確認するにはELLIPSISオプションのある状態で、 以下のように書きます:

>>> Foo()
<Foo object at ...>

例外のテストなども楽に記述できます:

>>> Foo().baaaaa
Traceback (most recent call last):
...
AttributeError: 'Foo' object has no attribute 'baaaaa'

それでは実装してみてください。使い方を書いて、リズミカルにRED, GREEN, RED, GREEN, ... と繰り返していきましょう。

注釈

次の節で、????部分をどのように書くかの参考例を提示します。 でもまずはそれをみないで自分で書いてみてください。 そして参考例とどちらが使いやすそうか比較してみるのも良いと思います。 良い利用例が出来たら、ぜひおしえてください。





















Hougan DocTestの参考例

参考までに私が実装する場合のDocTestを提示します。 ドキュメント部分は前述の通り、対話コード部分のみ抜粋です:

>>> hougan = Hougan()
>>> hougan
<Hougan (1x1) at ...>

>>> cell = hougan[0,0]
>>> cell
<Cell (0, 0), value=None, color=black at ...>

>>> cell.value is None
True
>>> cell.value = 1

>>> cell.color
0
>>> cell.color = 1

>>> hougan[1,1].value = 123

>>> hougan
<Hougan (2x2) at ...>

>>> print str(hougan)
= ===
1
  123
= ===

課題

  1. Range機能が必要になりました。DocTest駆動で実装してください:

    方眼紙の範囲を指定して、一括で値を変更する事が出来ます。
    
        >>> range = hougan[(0,0), (1,1)]
        >>> range
        <Range (0, 0)-(1, 1) at ...>
    
        >>> range.value = 10
        >>> range.color = 7
    
    実際に以下のように値が設定されました。
    
        >>> hougan[1,0].value
        10
    
  2. 座標の英数字表現機能が必要になりました。DocTest駆動で実装してください:

    方眼紙の座標は以下の記法でも指定することが出来ます。
    
        >>> hougan['A1']
        <Cell (0, 0), value=None, color=black at ...>
    
    方眼紙の範囲指定も以下の記法で行えます。
    
        >>> hougan['A1:B2']
        <Range (0, 0)-(1, 1) at ...>