« GoogleAnalyticsAPI on EC-CUBE | Yahooキーワード抽出APIライブラリ »

2009.06.05

テスト駆動開発 (test driven development: TDD) のすすめ

このエントリをはてなブックマークに追加このエントリをdel.icio.usに追加このエントリをLivedoor Clipに追加このエントリをYahoo!ブックマークに追加このエントリをPOOKMARK. Airlinesに追加このエントリをBuzzurl(バザール)に追加このエントリをnewsingに追加

テスト駆動開発 (TDD)とは、「プログラム開発手法の一種で、プログラム本体よりも先にテストケースを書くスタイル」(wikipediaより)のことです。テストケースとは作ったメソッドがどのように使われて、どのような振る舞いが想定されるかが分かるような検証用のコードです。TDDでは、多数のテストケースを書き、それらを自動的に実行できるツールによって、コードを検証し、開発を進めていきます。

この手法を取り入れるメリットは、いろいろあります。単なる品質保証だけではないことは確かです。ただ、実際に使ってみないと良さは分からないと思います。そこで、最近出版されたプロダクティブ・プログラマ ――プログラマのための生産性向上術の本の中に、よいサンプルコードがあったので、一部を簡単に紹介したいと思います。pythonが好きなので、コードはpythonで書きます。

完全数を求めるプログラムを書くという課題です。完全数とは、その数自身を除く約数の和が、その数と等しくなる自然数のことで、例えば 6 (=1+2+3)、28 (=1+2+4+7+14)などがあります。

まず、そのアルゴリズムを素直に書くと次のようになります:

def is_perfect(num):
    ''' 完全数の判定 '''

   # 引数の約数を全て求める
    factors = set([1,num])
    for i in range(2, int(math.sqrt(num)+1)):
        # 約数を取得
        if num % i == 0:
            factors.add(i)
            if num / i != i:
                # 約数のもう片方を登録(最適化のため)
                factors.add(num/i)

    # 約数の合計をとる
    sum = 0
    for fac in factors:
        sum += fac

    # 完全数かどうか判定する
    return sum - num == num

次に、このプログラムをTDDで作っていくとどうなるでしょうか?

まずは思いつく簡単なテストケースから書きます:

def test_factors_for_1():
    expected = set([1])
    c = Classifier(1)
    c.calculate_factors()
    assert c.get_factors() == expected

def test_factors_for_6():
    expected = set([1,2,3,6])
    c = Classifier(6)
    c.calculate_factors()
    assert c.get_factors() == expected

ここで、Classifierというクラスは、この段階ではまだ実装されてなくてよいものです。Classifierはコンストラクタの引数(上では1or6)の完全数を求める機能を担うクラスを想定しています。上のテストでは、「引数の約数を全て求める」部分を検証しています。assert文は、右にあるコードがTrueを返す場合はなにもせず、Falseの場合に例外を返すというものです。

さらに、もう少し、必要な機能要素を切り出し、テストケースを書いていきます:

def test_is_factor():
    assert Classifier(10).is_factor(1) == True
    assert Classifier(25).is_factor(5) == True
    assert Classifier(25).is_factor(6) == False

def test_add_factors():
    c = Classifier(20)
    c.add_factor(2)
    c.add_factor(4)
    c.add_factor(5)
    c.add_factor(10)

    expected = set([1,2,4,5,10,20])
    assert c.get_factors() == expected

さて、TDDでは上のテストコードを自動的に実行してくれるツールが必須です。pythonでは、テストツールとして組み込みのunittestモジュールがありますが、より簡単に実行できるnoseというモジュールがお勧めです。

noseをインストールするとnosetestsというコマンドが利用できるようになります。

nosetests xxx.py

とコマンドを打つと、xxx.pyにある名前が”test”で始まるメソッドが順次実行されます。

また、”test_hoge”というメソッドだけを実行したい場合は次のようにします。

nosetests xxx.py:test_hoge

ただし、次のようなコマンドオプションは入れたほうがいいと思います。

 -v: テストの成否を報告
 -s: プリント文を出力
 --nologcapture: loggingモジュールのログを出力

では、上のテストケースを実行してみましょう。…当然、Classifierの実装をしないと失敗しますね。TDDでは、それらテストを一つ一つクリアできるようにClassifierを作り上げていきます。そのような試行錯誤を経てできあがるClassifierは次のようになります:

class Classifier(object):
    def __init__(self, num):
        if num < 0:
            raise Exception("invalid number : %d", num)
        self.set_num(num)

    def get_factors(self):
        return self.factors

    def set_num(self, num):
        self.num = num
        self.factors = set([])
        self.factors.add(1)
        self.factors.add(num)

    def add_factor(self, i):
        if self.is_factor(i):
            self.factors.add(i)
            self.factors.add(self.num/i)

    def is_factor(self, factor):
        return self.num % factor == 0

    def calculate_factors(self):
        for i in range(2, int(math.sqrt(self.num) + 1)):
            self.add_factor(i)

    def _sum_of_factors_for(self, num):
        self.calculate_factors()
        sum = 0
        for fac in self.factors:
            sum += fac
        return sum

    def is_perfect(self):
        ''' 完全数の判定 '''
        return self._sum_of_factors_for(self.num) - self.num == self.num

この本体コードを、最初のナイーブなコードと見比べて見ましょう。

・コード数は増える
・小さいメソッドが多数ある
・コメントが少ない

という特徴が、TDD版のコードにあるのが分かります。元のナイーブなコードが構成要素に分解された形(メソッドが高凝集かつ疎結合)になったと言えます。こうなると、コードの再利用性が高くなり、コードの変更が容易になるというメリットがあります。

例えば、完全数だけでなく、過剰数(自分以外の約数の和が自分を上回る)か不足数(..下回る)の判定も行わせたい場合、TDD版のコードでは、下の単純な2つのメソッドを追加するだけですみます。

    def is_deficient(self, num):
        ''' 不足数の判定 '''
        return self._sum_of_factors_for(self.num) - self.num < self.num

    def is_abundant(self, num):
        ''' 過剰数の判定 '''
        return self._sum_of_factors_for(self.num) - self.num > self.num

もともと、こういう特徴を有するように本体コードを書けばいいという話もありますが、テストコードを書くことによって、必然的にそういうコードを書くようになることがポイントだと思います。

また、このような再利用性の高いコードができるだけでなく、ユーザー(自分自身も含む)視点のコードができるというメリットがあります。使えるメソッドだけ用意され、無駄なメソッドは書かなくなるという効果があると思います。無駄なメソッドを書くというのは、開発上無駄な部分というだけでなく、コードの複雑さを不用意にあげて、使いづらいものにしてしまいます。また、自然とサンプルコードが出来上がるという点も大きいと思います。ユーザーは、コード本体の内容よりも、使い方とその挙動が重要なので。

あと、やはりTDDでは、このようなコードが美しくなるというメリットとともに、テストケースを多数作り、それらを自動実行できることによって、一種のコンパイラ的な役割を果たすようになるというのが醍醐味だと思います。これによって、コードの修正がずっと怖くなくなり、よりよいプログラムへと素早く開発することができます。また、どこまでが正しいかが把握しやすいので、変な挙動を示すプログラムの修正すべき箇所を特定するのも、より容易になると思います。

とりあえず以上です。TDDに興味がでたでしょうか?API的なものを作るときはぜひ取り入れてみてください。

Trackback URL

Comment & Trackback

[...] テスト駆動開発 (test driven development: TDD) のすすめ [...]

[...] テスト駆動開発 (test driven development: TDD) のすすめ [...]

Comment feed

Comment





XHTML: You can use these tags:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>