慣習に従い 'PyUnit' と呼ばれる Python 単体テスト用フレームワーク (Python unit testing framework) は, 聡明なあの Kent Beck と Eric Gamma による JUnit の Python 言語版である。 ひるがえって JUnit は Kent's Smalltalk testing framework の Java 版である。 どちらもそれぞれの言語に対する単体テストの事実上の標準である。
この文書は PyUnit の設計と使用法の Python 特有の面を説明する。 フレームワークの基本設計についての背景情報については, 読者は Kent の原論文 "Simple Smalltalk Testing: With Patterns" を参照されたい。
PyUnit は Python バージョン 2.1. の Python 標準ライブラリの一部になっている。
以下の情報は Python の知識を仮定する。 この言語は非常に簡単なので私でさえなんとか学べたし, 非常に習慣性があるので私はもう止めることができない。
PyUnit はバージョン 1.5.2 以降のどの標準 Python でも動作するように設計されている。
PyUnit は著者によって Linux (Redhat 6.0, 6.1 と Debian Potato) 上の Python 1.5.2, 2.0, 2.1 でテストされている。 PyUnit は,Window と Mac を含む他の Python プラットフォームでも働くことが知られている。 何であれうまく動かないプラットフォームまたは Python のバージョンがあったなら,私に知らせてください。
PyUnit を JPython と Jython で使用する際の詳細については, 「PyUnit を JPython と Jython で使うには」. の節を参照してください。
テストを書くために必要なクラスは unittest モジュールにある。 このモジュールは Python 2.1 以降の標準 Python ライブラリの一部である。 もし君がもっと古いバージョンの Python を使っているなら, 別個に PyUnit ディストリビューションからモジュールを入手しなければならない。
このモジュールを君自身のコードから使えるようにするには,
ファイル unittest.py を置いてあるディレクトリを君の Python
の検索パスに入れるだけでよい。
そうするには環境変数 $PYTHONPATH をセットしてもよいし,
君の現在の Python 検索パスにあるディレクトリ,
たとえば Redhat Linux マシンなら
/usr/lib/python1.5/site-packages
などにファイルを置いてもよい。
これをするか, または unittest.py を例題のあるディレクトリにコピーしないと, PyUnit に同梱の例題を実行できないことに注意されたい。
単体テストを組み立てる基本的なブロックがテスト・ケースである。
ひとつのテスト・ケースはひとつのシナリオであり,
それぞれ前準備し正当性をチェックする必要がある。
PyUnit では,テスト・ケースは unittest モジュールの
TestCase クラスで表現される。
君自身のテスト・ケースを作るには,TestCase
の派生クラスを書く必要がある。
TestCase クラスのインスタンスは,
ひとつのテスト・メソッドを完全に実行できるオブジェクトである。
このメソッドにはオプションとして前準備と後片付けのコードが伴う。
TestCase
インスタンスの中でテストを行うコードは完全に自給式でなければならない。
つまり,単独または任意個数の他のテスト・ケースとの任意の組み合わせで
実行できなくてはならない。
最も単純なテスト・ケース派生クラスは,
特定のテストを行うコードを実行するために,
単純に runTestメソッドを上書き定義する。
import unittest
class DefaultWidgetSizeTestCase(unittest.TestCase):
def runTest(self):
widget = Widget("The widget")
assert widget.size() == (50,50), 'incorrect default size'
ただ単に Python 組込みの assert 文を使って,
テストをしていることに注意されたい。
もしもテスト・ケース実行時に assert 文が失敗すれば,
AssertionError がひき起こされ,
テストをするフレームワークはそのテスト・ケースを「失敗」(failure)
と認識する。
明示的な assert 検査からひき起こされたのではない他の例外は,
テストをするフレームワークから「エラー」(error) として認識される。
('テスト条件についての補足' 節 参照)
テスト・ケースの実行方法は後述する。 さしあたり,このようなテスト・ケースのインスタンスを構築するには, 引数なしでコンストラクタを呼ぶことに注意されたい。
testCase = DefaultWidgetSizeTestCase()
さて,このようなテスト・ケースは莫大な個数になり得る。 そしてその前準備はただの繰り返しになり得る。 上記のケースで,100 個のテスト・ケース派生クラスがそれぞれ Widget を構築するとしたら,それは見苦しい重複になるだろう。
さいわい,私たちは setUp という hook メソッドを実装して,
このような前準備コード (set-up code) をくくり出すことができる。
私たちがテストを実行するとき,
テストをするフレームワークが,
私たちに代わってそのメソッドを自動的に呼び出す。
import unittest
class SimpleWidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
class DefaultWidgetSizeTestCase(SimpleWidgetTestCase):
def runTest(self):
assert self.widget.size() == (50,50), 'incorrect default size'
class WidgetResizeTestCase(SimpleWidgetTestCase):
def runTest(self):
self.widget.resize(100,150)
assert self.widget.size() == (100,150), \
'wrong size after resize'
テストの実行中,もし setUp
メソッドが例外をひき起こしたなら,
フレームワークはテストがエラーに陥ったと見なし,
runTest メソッドを実行しない。
同様に,私たちは
runTest メソッドを実行した後片付けをする
tearDown メソッドを用意できる。
import unittest
class SimpleWidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
def tearDown(self):
self.widget.dispose()
self.widget = None
もし setUp が成功したなら,tearDown
メソッドは runTest の成否にかかわらず実行される。
テストをするコードのための,このような作業環境を作り付け (fixture) と呼ぶ。
多くの小さなテスト・ケースが同じ作り付けを使うことがよくある。
この場合,私たちは SimpleWidgetTestCase から
DefaultWidgetSizeTestCase
のような多くの小さな1メソッドのクラスを派生することに
終始してしまう。
これは時間の浪費であり勧められないから,JUnit と同じように,
PyUnit はもっと単純なしくみを用意している。
import unittest
class WidgetTestCase(unittest.TestCase):
def setUp(self):
self.widget = Widget("The widget")
def tearDown(self):
self.widget.dispose()
self.widget = None
def testDefaultSize(self):
assert self.widget.size() == (50,50), 'incorrect default size'
def testResize(self):
self.widget.resize(100,150)
assert self.widget.size() == (100,150), \
'wrong size after resize'
ここで私たちは,runTest メソッドの代わりに,
二つの異なったテスト・メソッドを用意した。
各クラス・インスタンスは今や test
メソッドの一つをそれぞれ実行する。
各インスタンスが個別に self.widget を構築し,破壊する。
私たちは,インスタンスを構築するとき,
実行すべきテスト・メソッドを指定しなければならない。
コンストラクタにメソッド名を渡すことで,これを指定する。
defaultSizeTestCase = WidgetTestCase("testDefaultSize")
resizeTestCase = WidgetTestCase("testResize")
テスト・ケースのインスタンスは,
テスト対象の機能にしたがってグループ化される。
PyUnit はこのためのしくみを用意している。
それが「テスト・スーツ」(test suite) である。
テスト・スーツは unittest
モジュール内の TestSuite クラスによって表現される。
widgetTestSuite = unittest.TestSuite()
widgetTestSuite.addTest(WidgetTestCase("testDefaultSize"))
widgetTestSuite.addTest(WidgetTestCase("testResize"))
テスト実行の便宜のため,後で見るように, 備え付けのテスト・スーツを返す callable なオブジェクトを, 各テスト・モジュールに用意しておくと良い。
def suite():
suite = unittest.TestSuite()
suite.addTest(WidgetTestCase("testDefaultSize"))
suite.addTest(WidgetTestCase("testResize"))
return suite
あるいは
class WidgetTestSuite(unittest.TestSuite):
def __init__(self):
unittest.TestSuite.__init__(self,map(WidgetTestCase,
("testDefaultSize",
"testResize")))
(明らかに後者は気の弱い人向けではない)
多くの似たような名前のテスト関数を引数にして
TestCase 派生クラスを作ることは共通のパターンだから,
unittest モジュールに
makeSuite という便宜関数が用意されている。
この関数は,
1個のテスト・ケース・クラスの中にあるすべてのテスト・ケースからなる
テスト・スーツを構築する。
suite = unittest.makeSuite(WidgetTestCase,'test')
makeSuite 関数を使う時,
テスト・スーツが様々なテスト・ケースを実行する順序は,
cmp
組込関数を使ってテスト関数の名前をソートして決定される順序であることに,
注意されたい。
しばしば,システム全体に対してテストを一度に実行するために,
テスト・ケースのスーツをグループ化したいことがある。
これは簡単である。
複数の TestCase を1個の TestSuite
に加えられるように,
複数の TestSuite を1個の TestSuite
に加えられるからである。
suite1 = module1.TheTestSuite()
suite2 = module2.TheTestSuite()
alltests = unittest.TestSuite((suite1, suite2))
テストスーツを入れ子にする例は,
配布パッケージの examples
サブディレクトリの alltests.py に見られる。
テスト・ケースとテスト・スーツの定義を, テスト対象のコードと同じモジュール (例えば 'widget.py') に置いてもよいが, テスト・コードを別個のモジュール,例えば 'widgettests.py' に置くことにはいくつかの利点がある。
もちろん,これらのテストを書く全体の目的は,
私たちがテストを実行できるようにすること,
そして私たちのソフトウェアが働くかどうか分かるようにすることである。
テスト・フレームワークは,いくつかの 'TestRunner' クラスを使って,
テストが実行できる環境を用意している。
最も一般的な TestRunner は TextTestRunner である。
これはテストを実行し,テキスト形式で結果を報告する。
runner = unittest.TextTestRunner()
runner.run(widgetTestSuite)
デフォルトでは,TextTestRunner はその出力を
sys.stderr に印字する。
しかし,
これはコンストラクタに別のファイル=ライクなオブジェクトを渡すことで,
変更できる。
このような TextTestRunner の使用は,Python
インタープリタのセッションから対話的にテストを実行する理想的な方法である。
unittest モジュールに main という関数がある。
この関数を使うと,テスト・モジュールを,
そのモジュール内のテストを実行するスクリプトへと簡単に変えられる。
main 関数は,unittest.TestLoader
クラスを使って,
現在のモジュール内のテスト・ケースを自動的に見付けてロードする。
したがって,もし君が前述の test*
という命名慣習を使ってテスト・メソッドを命名しているならば,
君のテスト・モジュールの末尾に下記のコードを置くことができる。
if __name__ == "__main__":
unittest.main()
すると,君がコマンド行から君のテスト・モジュールを実行した時, そこに含まれるテストがすべて実行される。 利用可能なオプションを見るには, '-h' オプションを付けてモジュールを実行すればよい。
コマンド行から任意のテストを実行するには,
unittest モジュールをスクリプトとして実行すればよい。
テスト・ケースまたはテスト・スーツの名前を引数として与える。
% python unittest.py widgettests.WidgetTestSuite
または
% python unittest.py widgettests.makeWidgetTestSuite
コマンド行で特定のテストを指定することもできる。
モジュール 'listtests' の
TestCase 派生クラスである 'ListTestCase'
(配布パッケージの 'examples' サブディレクトリ参照)
を走らせるには,次のコマンドを実行すればよい。
% python unittest.py listtests.ListTestCase.testAppend
ここで 'testAppend' は,
テスト・ケースのインスタンスが実行すべきテスト・メソッドの名前である。
ListTestCase クラスのすべての 'test*'
メソッドに対して同クラスのインスタンスを構築して走らせるには,
次を実行すればよい。
% python unittest.py listtests.ListTestCase
テストを実行するために利用できるグラフィカルなフロント・エンドがある。
それは,大多数のプラットフォーム上で Python
とともに出荷されているウィンドウ化ツールキットである
Tkinter を使って書かれている。
その外観は JUinit GUI と似ている。
GUI テスト・ランナを使うには,単純に次を実行する。
% python unittestgui.py
または
% python unittestgui.py widgettests.WidgetTestSuite
ここでも,やはり,実行すべきテストとして入力する名前は,
TestCase または TestSuite の
インスタンスを戻り値とするオブジェクトの完全修飾名
(fully-qualified name) でなければならない。
どのテストも実行のたびに再構築しなければならないから,
構築済みのテストの名前であってはいけない。
テキスト・テスト・ランナの代わりに GUI テスト・ランナを利用すると, 例のウィンドウ更新のために時間オーバヘッドが課せられる。 私のシステムでは,1000 テストあたり 7 秒余分にかかる。 この数値は環境により変わるかもしれない。
通常,テストの実行時には,
テストの名前が TestRunner によって表示される。
この名前は,テスト・ケースのクラス名と,
そのインスタンスが実行するように初期化されたテスト・メソッドの名前から,
導出される。
しかし,もし君がテスト・メソッドに doc-string を与えれば, その doc-string の最初の行が,テストの実行時に表示される。 これは君のテストをドキュメント化する簡単なしくみを用意する。
class WidgetTestCase(unittest.TestCase):
def testDefaultSize(self):
"""Check that widgets are created with correct default size"""
assert self.widget.size() == (50,50), 'incorrect default size'
私はテスト・ケースの条件をチェックするために
「自家製」の表明機構ではなく,
Python に組込みの表明機構を使うことを勧めてきた。
assert は単純であり,簡明であり,なじみもある。
しかし,テストを Python の
('.pyo' バイトコード・ファイルを生成する)
最適化オプション付きで実行した場合は,
assert 文はスキップされ,
テスト・ケースが役立たずになる。
Python の最適化オプションを有効にして作業したい人々のために,
私はメソッド assert_ を TestCase
クラスに含めた。
これは組込みの assert と機能的に等価だが,
最適化によって消えない。
しかし,より不便であり,より助けにならないエラー・メッセージに終わる。
def runTest(self):
self.assert_(self.widget.size() == (100,100), "size is wrong")
おまけとして,私は TestCase に
failIf と failUnless
のメソッドも用意した。
def runTest(self):
self.failIf(self.widget.size() <> (100,100))
テスト・ケースは fail を呼び出して,
即座に失敗することもできる。
def runTest(self):
...
if not hasattr(something, "blah"):
self.fail("blah missing")
# or just 'self.fail()'
最も一般的なタイプの表明は, 二つの値またはオブジェクトの間の等しさの表明である。 表明が失敗した時, 開発者はその不正値が実際はどんな値だったのかを見たいのが普通である。
TestCase には,この目的のために
assertEqual と assertNotEqual
というメソッドのペアがある。
(好みに応じて failUnlessEqual and failIfEqual
という別名もある)
def testSomething(self):
self.widget.resize(100,100)
self.assertEqual(self.widget.size, (100,100))
テストでは,しばしば, ある状況で例外がひき起こされることをチェックしたいことがある。 もし期待される例外が送出されなければ,テストは失敗しなければならない。 これは簡単にできる。
def runTest(self):
try:
self.widget.resize(-1,-1)
except ValueError:
pass
else:
fail("expected a ValueError")
普通,期待される例外の発生源は callable オブジェクトだから,
TestCase には assertRaises メソッドがある。
メソッドの最初の二つの引数は,
except 節に現われるような期待される例外と,callable オブジェクトである。
残りの引数は callable オブジェクトに渡される引数である。
def runTest(self):
self.assertRaises(ValueError, self.widget.resize, -1, -1)
既存のテスト・コードを持っていて,PyUnit から実行させたいが,
古いテスト関数を
TestCase 派生クラスにいちいち変換したくはない,
という利用者もいるだろう。
そのために,
PyUnit は FunctionTestCase クラスを用意している。
これは TestCase の派生クラスであり,
既存のテスト関数をラップするために使われる。
前準備と後片付けの関数もオプションとしてラップできる。
下記のテスト関数が与えられたとき,
def testSomething():
something = makeSomething()
assert something.name is not None
...
次のようにして等価なテスト・ケース・インスタンスを構築できる。
testcase = unittest.FunctionTestCase(testSomething)
もしテスト・ケースの操作の一部として呼び出されるべき付加的な, 前準備と後片付けのメソッドがあれば,それらも与えることができる。
testcase = unittest.FunctionTestCase(testSomething,
setUp=makeSomethingDB,
tearDown=deleteSomethingDB)
PyUnit は本来 'C' Python のために書かれたが, 君の Java または Jython のソフトウェアのために, Jython を使った PyUnit テストを書くことは可能である。 Jython を使って JUnit テストを書こうとするよりも, こちらのほうが好ましいかもしれない。 PyUnit は Jython の先祖である JPython 1.0 および 1.1 でも正しく動作する。
もちろん,Java は TK GUI インタフェースを持たないから, PyUnit の Tkinter ベースの GUI は Jython で動作しない。 しかし,テキストだけのインタフェースはバッチリ動作する。
そうするためには単に標準 C Python ライブラリ
モジュールのファイル 'traceback.py',
'linecache.py',
'stat.py', 'getopt.py'
を JPython から import できる場所にコピーするだけでよい。
これらのファイルは C Python
のどのディストリビューションからも入手できる。
(このガイドラインは C Python 1.5.x の標準ライブラリに基づいているから,
他のバージョンの Python には当てはまらないかもしれない)
そうすれば君は PyUnit テストを C Python のときと全く同様に書くことができる。
上述の"テスト条件についての補足" 節のただし書きを参照されたい。
テスト・スーツの実行中に例外がひき起こされたとき, 結果としての traceback オブジェクトが, 失敗の詳細をテスト実行の終わりに書式化して印字できるようにするため, 保留される。単純さはさておき,この利点は, traceback に格納されたローカルおよびグローバル変数値の検死調査が, 将来の GUI TestRunner にとって可能になり得るという点である。
潜在的な副作用は,失敗頻度が非常に高いテスト・スーツを実行したとき, これらの保留された traceback オブジェクト全体のメモリ使用が問題になり得るという点である。 もちろん,非常に多くのテストが失敗するとしたら, このメモリ・オーバヘッドは君にとって最も些細な問題である。
君はこのソフトウェアを,Python
それ自身に適用される同じく自由な条件の下で,
自由に利用,改変,および再配布してよい。
私が要求することはただ,私の名前と e-mail アドレスとプロジェクト URL
がソースコードおよび付随するドキュメンテーションに,
私を原著者として認める記述とともに,維持されることである。
You may freely use, alter and redistribute this software under the same
liberal terms that apply to Python itself. All I ask is that my name,
e-mail address and the project URL be retained in the source code and
accompanying documentation, with a credit for me as the original author.
私がこのソフトウェアを書いた動機は,
世界のソフトウェア品質の改善に小さな貢献をすることだった。
私は全然現金を得ることは期待しなかった。
(後援を歓迎しないと言っているわけではないです)
My motive for writing this software was to make a small contribution
to the improvement of software quality in the world; I didn't bargain
on getting any cash. (That's not to say that sponsorship would be
unwelcome.)
将来に向けての一つの主要な計画は TK GUI と IDLE IDE の統合です。 ボランティアを歓迎します!
それ以外,私にはモジュールの機能を拡張する大きな計画はありません。 私は PyUnit を出来る限り単純に (しかし,出来れば, 単純になりすぎないように!) 保ってきました。 なぜなら, ログ・ファイル比較などの共通のテスト作業のためのヘルパー・モジュールは, 私自身よりもテストの作者のほうがより良く書けると信ずるからです。
ニュース,アップデート,その他は プロジェクトのウェブサイト で入手できます。
コメント,提案,バグ報告を歓迎します。 単純に私に e-mail するか,または,こちらのほうがいっそう良いのですが, 非常に低流量の メーリング・リスト に入ってコメントを投稿してください。 驚くべき数の人々がすでに PyUnit を使っており, 彼らはみな共有すべき知恵を持っています。
Python 言語に対して Guido と彼の門弟に多いに感謝します。 感謝の印に,次の haiku (あるいは, もしそう呼びたいなら 'pyku') を書きました。
Guido van Rossum
'Gawky Dutchman' gave birth to
Beautiful Python
JUnit の労作に対して Kent Beck と Erich Gamma の功績を喜んで認めます。 それは PyUnit の設計を,頭脳をわずらわせないものにしました。
Tim Voght にも感謝します。 私が PyUnit を実装した後,彼もまた `pyunit' モジュールを彼の 'PyWiki' という WikiWikiWeb クローンの一部として実装していたのを発見しました。 彼は親切にも, 私が自分のバージョンを一般コミュニティに出すことを許してくださいました。
私に提案と質問のメールを書いた人々に多いに感謝します。 私はダウンロード・パッケージの CHANGES ファイルに適切なクレジットを書き加えるように努めました。
とりわけ Jérôme Marant に感謝します。 PyUnit を Debian 用にパッケージ化してくださいました。
Steve Purcell is just a programmer at heart, working independently writing, applying and teaching Open Source software.
He recently acted as Technical Director for a Web/WAP start-up, but spends most of his time architecting and coding large Java systems whilst counterproductively urging his Java-skilled colleagues to take up Python instead.