Testing your Application

テスト駆動開発は強力な開発方略です(またポピュラーになりつつあります)。TDDではコードを書く前にテストを書きます。このことは、そのコードが何をするのかということについての考えを固めることになりますし、どの様に構造化するのを確認することにもつながります。またTDDはモジュラー化などの良いアーキテクチャの設計を促します。何故ならそれにアクセスできないとテストできないからです。

最初にテストを書かなくても自動テストをすることで、避けられない要求が来てプログラムをそれに合うようにリファクタするようなときにその助けとなってくれます。

TurboGears?をインストールすると、 Nose も付いてきます。Jason Pellerinによって書かれたNoseは、ユニットテストに独自のディスカバリベースのテストランナーを追加拡張しているため、とても強力で便利です。

Noseを使うと、テストをするのに必要なのは、コンソールを開いてプロジェクトディレクトリで以下のコマンドを実行することだけになります:

nosetests

nosetests コマンドはディレクトリの中からすべてのテストケースを探し出し(test/Test/TESTを接頭語として持つクラスとメソッド)、テストを実行して報告してくれます。

ソース管理のリポジトリにコミットする前にテストを実行することで(ソース管理してますよね?)、プロダクトとして問題となってしまうような意図しない編集結果を知ることができます。早いうちに知ることができれば、追跡してバグの問題をシンプルにすることもできます。

テストの基礎

簡単なテストをしてみます:

# This method is a demo to be tested
def getsum(a,b):
    return a+b

# Test case are start with test_ 
def test_getsum():
    assert 3 == getsum(1,2)
    assert 4 != getsum(1,2)

既に述べたように、Noseは test で始まるモジュールを探します。この例では test_getsum() 関数を探し、 nosetests が1つのテストをパスしたことを報告するでしょう。便利のためにテストモジュールは、テストしているパッケージの下の階層にある "tests" という名前のパッケージに分離されますが、Noseは必ずしもそれを要求しません。

Noseは可能な限り目立たないようにしようとしますが、結果としてすべてのテストはPythonの assert 宣言を使って行われます。このassertはTrueかFalseを評価するので、 TrueFalse でないものについてはPythonのbooleanへ型変換を行うでしょう。 assert は2番目の引数として、人間が読むことのできるような記述を取ります:

assert 4 is getsum(1,2), "assert that 1 + 2 == 4"

これは nosetests -d を使ったテストが失敗したときに出力されるでしょう:

File "/path/to/file.py", line XX, in test_getsum:
  assert 4 is getsum(1,2), "assert that 1 + 2 == 4"
  AssertionError: assert 4 is getsum(1,2)
        >>  assert 4 is getsum(1,2), "assert that 1 + 2 == 4"

最後の行はnosetestsのassertによるもので、これは本来この行に表示される変数や値を置き換えます。

テストに例外が起こった場合はちょっとばかり変なことが出力されます:

try:
    test_int = int('five')
    assert False, "Should have raised ValueError"
except ValueError, e:
    assert "invalid literal" in str(e)

TurboGears?に特化したテスト: testutil モジュール

ウェブアプリケーションをテストするのは、そのほかの環境でテストすることほど簡単ではありません。リクエストの処理やデータベースのセットアップや解析などがあります。 turbogears.testutil モジュールはTurboGearsフレームワークそのものをテストするために書かれたモジュールですが、アプリケーションをもテストするためにTurboGearsフレームワーク内に取り込まれています。

ビューのテスト

例えば以下のような controllers.py ファイルをサンプルとして見てみましょう:

from turbogears import expose

class Root:
    @expose(html="projectname.templates.welcome")
    def index(self, value="0"):
        value = int(value)
        return dict(newvalue=value*2)

上記コントローラのテストモジュールは以下のようになります:

from turbogears import testutil
from projectname.controllers import Root
import cherrypy

##The template contains
#
# The new value is ${newvalue}.

# to test template
def test_withtemplate():
    "Tests the output passed through a template"
    cherrypy.root = Root()
    testutil.create_request("/?value=27")
    assert "The new value is 54." in cherrypy.response.body[0]

ここではNoseのセットアップや解析機能を使わずに、テスト関数を作ってすべてを行っています。docstringはテストが失敗したときにステキな出力をしてくれます。

CherryPyをテストするためにはルートオブジェクトをアクティブにする必要があります。これは cherrypy.root をセットすることで可能です:

cherrypy.root = Root()

このルートが生成されると、 testutil.create_request はすべてのCherryPyURL、関数デコレータ、テンプレート処理などに偽のリクエストを投げます。このリクエストが処理されると cherrypy.response に結果が送られますが、このレスポンスは何も送りません。

リクエストの処理が終わると cherrypy.response.body[0]? の中で修正されたものを in で探すことによって正しいかどうかをテストします。

コントローラのテスト

先ほどの例を引き続き使いながら、今度は(CherryPyやテンプレートは飛ばして) index() メソッドを直接呼ぶようなテストを書いてみましょう:

from turbogears import testutil
from projectname.controllers import Root
import cherrypy

# to test controller
def test_directcall():
    "Tests the output of the method without the template"
    root = Root()
    d = testutil.call(root.index, "5")
    assert d["newvalue"] == 10

CherryPyを介してはいませんが、やはりプロジェクトのルートインスタンスが必要になります。 testutil.call() メソッドはオブジェクトを返す関数を呼ぶために使われます。最初の引数はそのメソッドへのリファレンスであり、もう一つは *args**kwargs になります。コントローラへ返す値は call によって返され、テストされます。

モデルが cherrypy.request の中にある値を使っていたり、出力された辞書以外にも cherrypy.request をチェックしたい場合には、 testutil.call_with_request() を利用することができます。 call_with_request() はパラメータとしてメソッドリファレンスも request も取ることができ、メソッドの出力やオブジェクトのタプルを返します。リクエストオブジェクトがないときは、偽のオブジェクトを作るために testutil.DummyRequest? を使ってください。

モデルのテスト

モデルをテストするのはユニットテストにとってやっかいな問題です。何故ならその出力はふつうデータベースの状態に大きく依存するからです。すなわちテストからデータベースを分離する必要があるでしょう。これを簡単にするためにTurboGearsは testutil.DBTest? クラスを提供しています。

このクラスを継承すると、TurboGearsはそれぞれのメソッドに対してモデル中のテーブルをcreate/dropします。下記の例では、 test_model_reset()test_name() の後にあるにもかかわらず、完全に空のデータベース上で動きます。

from turbogears import testutil
from projectname import model

## from turbogears import database
## database.set_db_uri("sqlite:///:memory:") #this is the default

class TestMyURL(testutil.DBTest):
    model = model

    def test_name(self):
        entry = model.MyUrl(name="TurboGears",
              link="http://www.turbogears.com",
              description="cool python web framework")
        assert entry.name=='TurboGears'

    def test_model_reset(self):
        entry = list(model.MyUrl.select())
        assert len(entry) is 0

その他の機能

testutil はもともとTurboGearsフレームワークをテストするために作られたので、フレームワークの外で使うにはあまり便利ではないかもしれないたくさんのメソッドがありますが、一応最後まで説明するために列挙しておきます:

capture_log(category)
categoryは logging.getLogger() へ渡される名前になる( 'projectname.controllers' がデフォルトのコントローラロガー)。ロガーをリセットするためには print_log()get_log() を呼ばなければならない
print_log()
標準出力へログを表示し、ログをリセットする
get_log()
ログメッセージのリストを返し、ログをリセットする
catch_validation_errors(widget, value)
value を使った widget を生成しようとする。ウィジェットのタプルとInvalidなインスタンスのリストを返す
sqlalchemy_cleanup()
TurboGears?におけるSQLAlchemyの機能をすべてリセットする
reset_cp()
cherrypy.rootNone をセットする