2010-10-20

GAE アプリ用に PyUnit を使って単体テストを書く

単体テスト

(「アジャイルソフトウェア開発の奥義」p.34)
テストを最初に書くことによって、これまでとは違った視点で物事を見ざるをえなくなるということだ。テストを書こうとすれば、呼び出す側の立場でプログラムを見るようになる。[...snip...] テストを最初に書くことは、ソフトウェアを呼び出しやすい形式に設計することにつながるのだ。

その上、テストを最初に書こうとすれば、プログラムをテスト可能な形式に設計しようと努力するようになる。[...snip...] テストを最初に書くという行為は、ソフトウェアの分離を強いるのだ

もうひとつの効果は、テストそのものが有益なドキュメントになるということである。[...snip...] この「ドキュメント」はコンパイルも実行も可能であるばかりでなく、常に最新だし、間違いはありえあない。

本体のコードを書く前にテストコードから書き始める、というテスト駆動開発の教義を文字通り実行しないとしても、テストを書くことで違った視点が得られることは確かだ。また、これも良く言われるようにリファクタリングを実行するための後押しにもなる。

そして、何より大事なことは、テストは「動くドキュメント」になるという点だ。

PyUnit

Python には標準添付ライブラリに、単体テストを書き、実行するためのモジュール(unittest)がふくまれている。このモジュールは JUnit の Python 版と言われるだけあって、JUnit を使ったことがあれば、テスト実行の仕組みを理解するのにも、実際にテストを書くのにも苦労しないはずだ。

(単体テストサンプル)
import unittest

# target module 
import foo

class FooTest(unittest.TestCase):
    def test_bar(self):
        self.assert_([...snip...])
    [...snip...]

GAE アプリに適用するには

テスト用のディレクトリを用意する

Python 版の GAE アプリではアプリのソースコードがそのままサーバに送られる。だが、テスト自体のコードをサーバに送るのはムダだ。となれば、アプリのソースとテストはディレクトリを分ける方が良い。

たとえば、こんな風に。

(GAE アプリのディレクトリ構造)
$(APPTOP)/
    +--- src/
    |    +--- app.yaml
    |    +--- main.py
    |    |    ...
    +--- tests/
         +--- run_all_tests.py
         |    ...
モジュールサーチパス

テストをアプリのコードとは別のディレクトリに置く場合、そのままでは import に失敗する。tests ディレクトリにあるテストから src にあるテストの対象となるモジュールを import するには、src をモジュールサーチパスに入れてやらなければならない。

また、GAE のコードであれば、SDK のモジュールをあれこれ import しているはずだから、これもモジュールサーチパスに入れておく必要がある。GData ライブラリのような外部ライブラリを使っているのであればそれも。

今回、モジュールサーチパスを設定するために用意したコードがこれになる。アプリコード用の src に加え、GData ライブラリの atomgdata、そして GAE の SDK のパスも入れてある。

(tests/pathconf.py)
 1: #!/usr/bin/python2.5
 2: 
 3: import os
 4: import sys
 5: 
 6: current_dir = os.path.dirname(__file__)
 7: extra_path = [
 8:     current_dir + '/../src',
 9:     current_dir + '/../src/atom',
10:     current_dir + '/../src/gdata',
11:     '/usr/local/google_appengine',
12:     '/usr/local/google_appengine/lib/antlr3',
13:     '/usr/local/google_appengine/lib/django',
14:     '/usr/local/google_appengine/lib/fancy_urllib',
15:     '/usr/local/google_appengine/lib/ipaddr',
16:     '/usr/local/google_appengine/lib/webob',
17:     '/usr/local/google_appengine/lib/yaml/lib',
18:     ]
19: sys.path = extra_path + sys.path
20: 
21: if __name__ == '__main__':
22:     print sys.path

したがって、実際のテストは以下のように書く。

(tests/skelton_testcase.py)
 1: #!/usr/bin/python2.5
 2: 
 3: import unittest
 4: 
 5: import pathconf
 6: # target module 
 7: import foo
 8: 
 9: class FooTest(unittest.TestCase):
10:     def test_bar(self):
11:         self.assert_(False)
12: 
13: def suite():
14:     return unittest.TestSuite((
15:             unittest.makeSuite(FooTest, 'test'),
16:             ))
17: 
18: if __name__ == '__main__':
19:     unittest.TextTestRunner().run(suite())
テストできない機能も出てくる

単にモジュールサーチパスを整えただけでは、GAE アプリの実行環境すべてをシミュレートできるわけではない。当然、単体テスト環境では実行できない機能も出てくる。

その制約の中で、いかにテスト対象のコードを GAE の実行環境から独立させるかを考えることが、結果としてコードの独立性を高めることにつながるのだ。

テストを書くだけでも効果がある(かも)

Blogger Glass (以下、BG)にテストを書き始めて(というより、テストの組み込み方をあれこれ試していて)すぐにバグが 1 つ見つかった。BG ではsrc/config.py でアプリの設定を src/config.yaml というファイルから読み込んでいる。その部分のコードは以下のようになっていた。

(src/config.py より)
settings = {}

with open("config.yaml") as f:
    for line in f:
        md = re.search('^([^:]+):\s*([^\s].*)$', line)
        if md:
            settings[md.group(1)] = md.group(2)

これを test ディレクトリに置いたテストケースから import すると、config.yaml が見つからずエラーになる。アプリのコードでは import するコード (最新版では src/util.py) も同じディレクトリにあるためエラーにならない。しかし、それでは config モジュールを利用する側に条件を付けることになってしまう。言葉を換えれば情報隠蔽に失敗している。

このコードの open は、正しくは以下のように書かれるべきだったのだ。

(src/config.py の修正)
with open(os.path.join(os.path.dirname(__file__), "config.yaml")) as f:

これなら、src 以外のディレクトリから import してもエラーにはならない。

テストを書こうとすれば、コードを違った視点で見るようになることも、分離を強いられる(コンポーネントの独立性が高まると言っても良い)ことも間違いないようだ。

参考文献

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技
ロバート・C・マーチン
ソフトバンククリエイティブ ( 2008-07-01 )
ISBN: 9784797347784
おすすめ度:アマゾンおすすめ度

「第4章 テスティング」がテストに関する内容。前半が単体テストを使ったテスト駆動開発について書かれたものになっている。

初めてのPython 第3版
Mark Lutz
オライリージャパン ( 2009-02-26 )
ISBN: 9784873113937
おすすめ度:アマゾンおすすめ度

モジュールサーチパスについては、p.416 〜 421 に記述がある。

関連リンク

関連記事

0 件のコメント:

コメントを投稿