ユニットテスト
翻訳者註(2009.11.26):この文書は http://plone.org/documentation/tutorial/richdocument/unit-testing に記載されている文書を翻訳したものです。対象バージョンが Plone 2.1.x の頃のドキュメントですが、ほとんどの部分が Plone 3.3.x でも通用する内容です。 2010年5月6日にテスト実行方法を寺田が追記しました。
UnitTestは、Plone 2.1の機能として厳密はに定義されていないが、これは非常に重要な "良い手法" であり、慣れ親しんでおくべき事だ。Ploneは広範囲にわたってUnitTestを実施している(UnitTest無しでPloneのコードをチェックしようとしてはいけない, 時間がいくらあっても足りない)。ここでUnitTestの導入について簡単に説明しよう。さあ、始めよう!
UnitTestのアイディアは、ソフトウェアの部品が集まり複雑に結合してくるとテストがだんだん難しくなってくるという点にある。君がPloneのUI上で動作する自身の製品をテストしていて何かをおかしくしてしまったとき、以前に動作していたと思われる全てのテストを再度行おうとすることはとても難しい事だ。
UnitTestには以下のゴールデンルールがある
- 一つの機能ごとに最低一つのテストを書く
- (可能なら)インターフェースとスタブ関数を最初に書き、次にテストを書く。このテストは必ず失敗するだろう(コードをまだ書いていないから!)
- テストに失敗した時だけ、機能を実装する事が許される。全てのコードはテストを通すことだけを考えて書くこと
- バグを見つけても直さないこと...
- ...その代わり、そのバグを再現するテストを書いて...
- ...そしてテストを通すためにそのバグを修正する事が許される
いや、からかってる訳じゃない。これらは厄介で直感的でないように見えるかもしれないが、でもそれは間違っている。UnitTestというのは、
- 遠く離れたクライアントや友人たちに君のコードが壊れていない事を証明できる唯一の方法
- 君がよくしらない何かを壊してしまわないようにする確実な方法
- 君がコードを修正しようとして新たなバグを入れ込んでしまわないようにする確実な方法
- 君が何かをおかしくしてしまったときに、6ヶ月前に書いたコードに潜む不鮮明なバグを追跡する時間を節約するための普遍的な方法
- コードを書いてテストするのに役に立つ方法 - 新しい機能のテストのためにブラウザであちこちクリックする必要はない - ちょっとテストを走らせるだけだ!
良いスコッチウィスキーのようにUnitTestをパスすることは心地よいことだ。そして時間とともにテストのカバレッジは上がっていき、どんどん良くなっていく。Ploneには、これを書いている時点で1500以上のテストケースがあって、全てパスしている。 でも実際のところ、今のPloneのコードにはその3倍のテストが必要だろう。
Zope/Plone UnitTest
UnitTestはSandBox(あるいはTest fixtureと呼ばれる)をセットアップし、その中でテストを実行する。PloneTestCaseを使うと、空のZopeインスタンスの中にPloneサイトを一つ作ってくれる。このPloneサイトには一人のメンバーと、デフォルトのメンバーフォルダも含まれている。全てのテストはこの環境で実行される - すなわち、毎回テスト関数を実行しおわる毎にZopeのトランザクションを失敗させることで、テスト関数が変更した全ての状態が元に戻り、次のテストは完全にそれまでのテストと独立して実行することが出来る。だからテストの中でどんな恐ろしいことをしても大丈夫 - PloneのtestMigrations.pyテストのように。僕たちはportal_typesを気軽に削除することが出来る。テストの実行順序は保証されない(多分アルファベット順だけど)ので、実行順をあてにしたテストを書くべきではない。
以下のように、PloneTestCaseを使ってUnitTestを書くためのいくつかの基本的なルールがある:
- 最初にテストを書く。これを怠ったり後回しにしてはいけない :)
- テストしたい事それぞれについて一つのテスト(i.e. 関数)を書く
- 関連するテストを一緒に書く(i.e. 同じクラス内等)
- 実践的に。全ての入力と出力の組み合わせをテストするのはばかげてるし、そういったテストにそれほど価値があるとは思えない。同様に、もし一つの関数が複雑な場合、基本的なケースだけをテストするなんて事をしてはいけない。これは経験から来る事だけど、一般的に言えることとして、テストは、よくあるケース、境界値のケース、そしてメソッドかコンポートネントが失敗するようなテストを実施するべきだ(i.e. test that it fails as expected).
- シンプルさを保て。賢くあろうとするな、必要以上に一般化するな。テストが失敗したときに、簡単に失敗の原因がコードにあるのか、それともテストにあるのかを知る必要がある
- コードをチェックする前にいつも全てのテストを走らせて(特にオリジナルコードの原作者/メンテナでは無い場合)、どこも壊れていないことを確認すること。これをせずにPloneのコアをいじることはとても恐ろしいことだ。それは君の環境にも言える。
テスト環境をセットアップしよう
君のプロダクトにUnitTestを追加するのはとても簡単だ。やらなきゃいけないことは:
- PloneTestCaseとそれに関連するものをインストールする(INSTALL.txtを参照。ZopeTestCaseとPloneTestCaseやその関連コードはZope 2.8以降に依存している事に注意)
- tests/ ディレクトリを作って、 runalltests.py と framework.py をその中に入れる。 それらは RichDocument/tests フォルダや、その他色々なパッケージの中に入っている
- いくつかのテストメソッドを含むテストケースクラスを追加する。テスト環境のセットアップを簡単に一貫性のある物にするために、テストケースのためのベースクラスを定義するのが良いだろう。RichDocumentで言えば rdtc.py が参考になる。これは testSetup.py から使われ、実際のテストメソッドを含んでいる。
RichDocument のテストにはまた後で戻ってくるとして、まずUnitTestがどうやって実行されるのかを見てみよう!
テストを走らせる
UnitTestを走らせるには3つの異なる方法がある。一番簡単なのはZopeインスタンスのルートでzopectlを以下のように実行する事だ:
./bin/zopectl test --libdir Products/RichDocument
Plone3.xでは下記の様に実行します(2010年5月6日寺田追記):
./bin/instance test -s Products.RichDocument
Plone4ではtestが実行できるようにセットアップし下記の様に実行します(2010年5月6日寺田追記):
./bin/test -s Products.RichDocument
他の方法としては、testsディレクトリに移動して(例, Products/RichDocument/tests) 直接テストを実行する方法がある:
python testSetup.py
あるいは:
python runalltests.py
どっちの場合も、環境変数 INSTANCE_HOME と SOFTWARE_HOME を設定しておく必要がある。前者はZopeインスタンスのあるディレクトリ(Productsフォルダの親)を指し、後者はZopeがインストールしたPythonライブラリの場所を指している必要がある(例, /usr/local/zope-2.8.4/lib/python)。
3つめの例 runalltests.py は最初の zopectl を使った例と等価だけど、より気軽に利用できるだろう。第一に、テストファイルを直接呼び出す方法はとても役に立つ - つまりテストの一部だけを実行することが出来るようになる。テストに時間がかかるほどの大きなプロジェクトになると、君に関係のあるテストだけを実行するのはこの場合良い方法だろう。そして、君に関係のあると思っているテストが全部パスしたら、その時こそ全てのテストを実行してドコモ壊れていないことを確認しよう。(No, we don't care that you are writing your code in a totally different python module than what those other tests are supposed to test, and that they were all fine and good and all you changed was a docstring. 君が出来たと思ったときにテストを走らせればいいんだ)
テストが完了すると次のようなレポートが表示させる:
... Ran 18 tests in 6.463s OK
"OK"の表示が出たら、テストの失敗が表示されなくて良かった、とため息をついてほっとする練習をしておこう。この表示が出たら、君は svn commit して現実の生活に戻って寝るなり友達に会いに行くなりすることが出来る。
もし不運なことに次のように表示されたら:
Ran 18 tests in 7.009s FAILED (failures=1, errors=1)
これは1つのpythonエラーと、1つのテストの失敗を表している。
pythonエラーは、テストコードか、あるいはテストから呼ばれるコードのどこかで冷害が発生している事を意味している。これは良くない。すぐに直してしまおう。
テストの失敗は、テストを実施したけれどどこかでassertに引っかかっていることを意味している。これはOKだ。なぜなら君はまだテストされるコードを書いていないはずなんだ(ちゃんとテストを先に書いたね!)。あるいはまだ失敗の原因を知らないだけだ。時々コードを根本的にリファクタリングするか、書き直したくなることがあるかもしれない。その作業の間テストは失敗を報告し続けるだろう。こういった事を出来るのが、UnitTestのとても良い所の一つだ。
時には(常にではない。リリースマネージャにOKをもらわずにPloneコアでやろうとしてはいけない)、テストの失敗を修正する方法を知らないのであれば、それを放置することも許されます。修正方法を知っている他の開発者が気が付いて、修正してくれるかもしれない。
UnitTestを書こう
さあ、これでテストのセットアップと走らせ方は分かった。次は実際に書いてみよう。UnitTestは例から学ぶのが一番よい(Unit testing is an art form that is best learnt by example.)。君が関係ありそうだと思うであろうRichDocumentやPloneや他のプロダクトなどのUnitTestを見て、それらがどのように動作しているか、それらがどのようなソートを使っているかを見ると勇気づけられるだろう。
Zope/PloneのUnitTestは、いくつかの命名規則をもっていて、君がテストを書くときの煩わしさを軽減させるだろう。基本的には:
- 全てのテスト用 .py ファイルは、 testSetup.py のように test で始まる名前でなければいけない
- テストファイルにテストケースのクラスを定義する。各クラスにいくつかのテストメソッドを持たせる事ができるが、そのメソッドは testSkinLayersInstalled() のように test で始まる名前でなければいけない
さあ、例を見てみよう。rdtc.pyにRichDocumentのための最良のテストケースがある:
# これらはインストールだけ行う(あるいは失敗する) ZopeTestCase.installProduct('CMFCore', quiet=1) ... ZopeTestCase.installProduct('kupu', quiet=1) # これらはきれいな状態でインストールされる ZopeTestCase.installProduct('RichDocument') PRODUCTS = ['RichDocument'] PloneTestCase.setupPloneSite(products=PRODUCTS) class RichDocumentTestCase(PloneTestCase.PloneTestCase): ....
上記の各行では、ZopeTestCaseを使って標準のプロダクトを登録し、Ploneサイトと RichDoocument をインストールしている。次に、全てのテストケースクラスのベースクラスが定義されている。この例ではただ定型の決まった処理だけをしている。他のプロダクトでは、各テストから呼び出される便利なユーティリティーメソッドを持つ例もあるだろう。
RichDocumentのUnitTestファイル testSetup.py ではまず (and to date only) 以下のコードがある:
import os, sys if __name__ == '__main__': execfile(os.path.join(sys.path[0], 'framework.py')) from Products.RichDocument.tests import rdtc class TestInstallation(rdtc.RichDocumentTestCase): """Ensure product is properly installed""" def afterSetUp(self): self.css = self.portal.portal_css self.kupu = self.portal.kupu_library_tool self.skins = self.portal.portal_skins self.types = self.portal.portal_types self.factory = self.portal.portal_factory self.workflow = self.portal.portal_workflow self.properties = self.portal.portal_properties self.metaTypes = ('RichDocument', 'ImageAttachment', 'FileAttachment') def testSkinLayersInstalled(self): self.failUnless('RichDocument' in self.skins.objectIds()) self.failUnless('attachment_widgets' in self.skins.objectIds()) ... def test_suite(): from unittest import TestSuite, makeSuite suite = TestSuite() suite.addTest(makeSuite(TestInstallation)) ... return suite if __name__ = '__main__': framework()
一番最初の数行に、 python testSetup.py のようにしてテストを個別に実行するためのpythonコードの手法が書かれている。次に、テストクラスを定義し、最後に、テストスイートが定義されている。各テストスイートに君はいくつかのテストクラスを追加することが出来る。テストスイートを走らせると(例, python testSetup.py) ファイル内のテストスイートに含まれる全てのテストクラスの全てのテストメソッドが実行される。
from unittest import ... の行に気をつけて欲しい。これはPython標準のunittestモジュールからテスト機構を読み込み、ZopeTestCaseで使えるようにしている。詳しくは unittestモジュール :: Pythonドキュメント を参照して欲しい。
TestInstallation で定義している各テストケースが実行される前に afterSetUp() メソッドが毎回必ず呼び出され、各テストのためのデータなどをセットアップすることができる。よくある使い方としては、toolsを取得したりダミーコンテンツを生成したり、色々だ。テスト実行後にはトランザクションがロールバックされからクリーンアップのためのコードを書いたりする必要はない。でももしテストを行うことでZopeの外のデータベースや何かのデータを変更してしまうなら、各テストの終了時にクリーンアップを実施するために beforeTearDown() を実装しよう。
君が必要だと思うどんなヘルパーメソッドもUnitTestクラスに追加することが出来る。ただし test... で始まるメソッドはテストとして実行されてしまうことに注意しよう。テストは普通出来るだけ簡潔に書かれなければいけない(不明瞭な書き方で混乱しないように)。テストコードでは self.assertEqual() や self.failUnless() といったメソッドを使うことが出来る。これらのアサーションメソッドが実際のテストだと言える。もしこれらのメソッドが失敗した場合、テストは失敗(failure)カウントを1増やし、君はテスト結果として見たくもないFマークを見ることになるだろう。
UnitTestフレームワークのアサーションとユーティリティーメソッド
かなり多くのアサーションメソッドがあるけど、その多くはほとんど同じ動作 - 結果が True か False かをチェックする。君がテストを作るのにどの名前のアサーションメソッドを使ってもかまわない。実際の所、テストコードは、君のコードにどのような振る舞いを期待しているのかを表している。そういったテストコードは、他の開発者たちが君のコードがどういったものなのかを知るために非常に便利だ。アサーションメソッドの一覧は unittest.TestCase :: Pythonドキュメント で見ることが出来る。以下に、その中でもっとも使われるものを挙げる:
- failUnless(expr)
- expr が真になることを期待する
- assertEqual(expr1, expr2)
- expr1 と expr2 がイコールとなることを期待する
- assertRaises(exception, callable, ...)
- callable を呼び出して exception が発生するのを期待する。ただし、callableはメソッドかcallableなオブジェクトの名前であり、assertRaisesを書くときに実際に呼び出してはいけないという事に注意して欲しい。例えば self.assertRaises(AttributeError, myMethod, someParameter) と書く。このときに myMethod の後ろに () を付けてはいけない。もし付けてしまうと、テストメソッド内で例外が発生してしまい、これは多分期待した動作ではないだろう。上記の例は、テストフレームワーク内で myMethod(someParameter) のように呼び出され(パラメータの個数は好きな数だけ渡せる)、AttributeErrorがチェックされる。
- fail()
- 失敗する。テストが完了する前か、 if 文でテストが失敗の方向に分岐したときに呼び出すことで、テストを失敗させる。
UnitTestフレームワークが提供するアサーションメソッド、ZopeTestCase、PloneTestCaseに含まれる多くのヘルパーメソッド、そしてZopeと対話する助けとなる変数などについて追記しよう。本当はZopeTestCaseとPloneTestCaseプロダクトのソースコードを読むのがためになるけど、キーとなる変数についていくつか簡単に書こう:
- self.portal
- テストが実行されているPloneポータル
- self.folder
- テストを実行するユーザーのメンバーフォルダ
次に、キーとなるメソッドについて:
- self.logout()
- ログアウトする。例えばAnonymousになりたいとき。
- self.login()
- 再度ログインする。ユーザー名を渡すと、それまでとは異なるユーザーとしてログインできる
- self.setRoles(roles)
- ロールのリストを渡して権限を設定する。例えば self.setRoles((Manager,)) とするとマネージャーになることが出来る。
- self.setPermissions(permissions)
- 同様にself.folderにテストユーザーのパーミッションを設定出来る。
- self.setGroups(groups)
- 現在のユーザーにグループのリストを設定する。
Tips & Tricks
良いUnitTestは経験から作られる。コードがかなり詳しく書かれたUnitTestや、他の人のUnitTestのコードを読むことはいつも役に立つことだ。あとは君がUnitTestに取り組むためのアプローチを決めるためのいくつかのヒントについて書こう:
- 臆病にならないで!Pythonは動的で型付けの弱い言語なので、どんなへんな事も出来る。だから、君はテストの目的を果たすためにPloneのコアから機能を抜き出して自分の afterSetUp() 実装に差し替えたりテストしたりすることが出来る。
- 同様に、MailHostをダミーオブジェクトに差し替える、と言ったような事もテストには必要になるかもしれない。 CMFPlone/tests/dummy.py を見ると、いくつものダミーオブジェクトの例を見ることが出来る。
- 各テストは何をやってもいい。安全な環境がある。もしなにか試す必要があるならそれをテストの中に書くことで働きを簡単に観察することが出来る。
- デバッグ中は print 文をテストコードの中に書くことでテストの実行状況をターミナルで追跡しやすくすることが出来る。でもこれの方法に依存しないようにしてください。:)
- 同様に、pythonデバッガはテスト中に非常に柔軟に使える。 import pdb; pdb.set_trace() の一文をテストメソッドの中に置くことで、君はコードをステップ実行してテストの処理を追う事が出来る。もしpythonデバッガに詳しくないのなら、君の人生は不完全打だろう。次のドキュメントをよく読んでおこう pdbモジュール :: Pythonドキュメント.