単体テスト、結合テスト、機能テスト

単体テスト (unit testing) は、読んで字のごとく、アプリケーションの中の 「ユニット (単位)」をテストする行為です。この文脈で「ユニット」とは、多くの 場合関数またはクラスインスタンスのメソッドです。ユニットは「テスト単位 (unit under test)」とも呼ばれます。

単一の単体テストのゴールは、テスト単位の特定の組み合わせ のみ をテストすることです。 Python 関数によって特定のコードパスの結果を確認 することを目標とする単体テストを書く場合、 関数本体それ自体に含まれる コードのテストについてのみ関心を払う必要があります。関数が (リソース、 データベース接続あるいは SMTP サーバーのような) 複雑なアプリケーション 「ドメインオブジェクト」を表わすパラメータを受け付ける場合、単体テスト の間この関数に提供される引数は、「本物の」実装オブジェクト である必要 はなく 、そしておそらく そうであるべきではありません 。例えば、ある 関数の実装が SMTP サーバーオブジェクトを表わす引数を受け取り、システム が通常動作している場合にこのオブジェクトのメソッドを呼ぶことによって電子 メールが送られるかもしれませんが、この関数のこのコードパスの単体テストは、 電子メールが実際に送られることをテストする必要は ありません 。 それは、単に引数として渡されたオブジェクト (もしその引数が SMTP サーバー オブジェクトの「実際の」実装であったなら、電子メールを送った だろう オブジェクト) のメソッドをその関数が呼ぶことを確かめる必要があるだけです。

結合テスト (integration test) は、それに対して、2つ以上の「ユニット」間の 相互作用が明示的にテストされる、テストの異なる形式です。結合テストは、 アプリケーションの複数のコンポーネントが一緒に動くことを確認します。 結合テストの中で電子メールが実際に送られたことを確かめることは ありえます

機能テスト (functional test) は、アプリケーションが「文字通りに」実行される 結合テストの一形式です。機能テストでは電子メールが実際に送られることを 確かめ なければならない でしょう。なぜならそれはコードを端から端まで テストするからです。

いかなるコードベースに対しても、それぞれの種類のテストを書くことは、 しばしばベストプラクティスと考えられます。単体テストは、しばしばより良い 「カバレッジ」を得る機会を提供します: テスト単位に、その すべての 潜在的なコードパスを実行する引数や環境を提供することは通常可能です。 結合テストや機能テストでこれを行うことは通常容易ではありませんが、 結合テストと機能テストは「ユニット」が一緒に動作するという保証 (それは アプリケーションがプロダクション環境で実行されるときに期待されることです) の measure (基準, 評価, 手段) を提供します。

Pyramid アプリケーションで推奨されているユニットテストおよび結合 テストのメカニズムは、 Python の unittest モジュールです。 このモジュールは unittest という名前ですが、実際には単体テストと 結合テストの両方を実行することができます。単体テストの良いチュートリアルは Mark Pilgrim による Dive Into Python にあります。

Pyramid は、単体テスト、結合テスト、機能テストを簡単に書けるように するための能力を多く提供しています。その能力は、コードが Pyramid 関連の フレームワーク機能を呼び出す場合に特に有用になります。

テストの set up と tear down

Pyramid は、現在の request と現在の application registry という 2 つの要素を保持するために「グローバル」 (実際には thread local) なデータ構造を使用します。これらのデータ 構造はそれぞれ pyramid.threadlocal.get_current_request() 関数と pyramid.threadlocal.get_current_registry() 関数で得ることが可能です。 これらの関数、およびそれらが返すデータ構造に関する情報については、 Thread Locals を参照してください。

あなたのコードがこれらの get_current_* 関数を使用するか、 get_current_* 関数を使用する Pyramid コードを呼んでいるなら、 テストの setup で pyramid.testing.setUp() を呼び、テストの teardown で tearDown() を呼ぶ必要があるでしょう。 pyramid.testing.setUp() はレジストリを thread local スタックに push して get_current_* 関数が動くようにします。 pyramid.testing.setUp() は、テストコードで必要な追加の設定を 行なうために使用できる Configurator オブジェクトを返します。 tearDown() はスレッドローカルのスタックを pop します。

通常 Configurator が Pyramid アプリケーションの main ブロックで 直接使用される場合、 .commit メソッドが (しばしば pyramid.config.Configurator.make_wsgi_app() メソッドによって暗黙的に) 呼ばれるまでは「実際の仕事」の実行は延期されます。しかし、 pyramid.testing.setUp() によって返された Configurator は、 autocommitting Configurator です。それに対して呼び出されるメソッドで 指示されたすべてのアクションは直ちに実行されます。単体テストを行う上で、 これは各テストに必要な設定文を追加した後に pyramid.config.Configurator.commit() を呼ぶ必要がないため便利です。

setUp() 関数と tearDown() 関数を使用することで、テストケースの各単体テストメソッドに対し単一のテストに 隔離されたレジストリとリクエストが存在する環境を提供することができます。 以下は、この機能を使用する例です:

1
2
3
4
5
6
7
8
9
import unittest
from pyramid import testing

class MyTest(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()

    def tearDown(self):
        testing.tearDown()

上記の例は、 MyTest のテストケースメソッド内で呼び出された pyramid.threadlocal.get_current_registry()config Configurator インスタンスに関連付けられた application registry を返すことを保証します。 MyTest に取り付けられたテストケースメソッドは、 それぞれが隔離されたレジストリを使用するようになります。

pyramid.testing.setUp() 関数と pyramid.testing.tearDown() 関数は、 テスト環境に影響を及ぼす様々な引数を受け取ります。これらの関数でサポートされる 追加の引数に関する情報については pyramid.testing 章を参照してください。

さらに、単一のテストの間に get_current_request()None 以外の何かを返させたければ、 テストの setUp メソッド内で pyramid.testing.setUp()request オブジェクトを渡すことができます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import unittest
from pyramid import testing

class MyTest(unittest.TestCase):
    def setUp(self):
        request = testing.DummyRequest()
        self.config = testing.setUp(request=request)

    def tearDown(self):
        testing.tearDown()

テストケースの setUp 内で pyramid.testing.setUp()request オブジェクトを渡した場合、 get_current_request() を直接または間接的に 呼び出す MyTest テストケースに配置された任意のテストメソッドは、 その request オブジェクトを受け取ります。そうでなければ、テスト中に get_current_request()None を返します。 「本物の」 Pyramid リクエストオブジェクトを構築するより簡単なので、 ここでは pyramid.testing.DummyRequest によって提供される「ダミー」 リクエスト実装を使用しています。

コンテキストマネージャーを使ったテストのセットアップ

テスト設定のセットアップの別のスタイルは、コンテキストマネージャーを 作るために with 文と pyramid.testing.testConfig() を使用することです。このコンテキストマネージャーは、テスト下のコードの 前に pyramid.testing.setUp() を、後で pyramid.testing.tearDown() を呼び出します。

このスタイルは、小さな自己完結したテストで便利です。例えば:

1
2
3
4
5
6
7
8
9
import unittest

class MyTest(unittest.TestCase):

    def test_my_function(self):
        from pyramid import testing
        with testing.testConfig() as config:
            config.add_route('bar', '/bar/{id}')
            my_function_which_needs_route_bar()

What?

スレッドローカルデータ構造は、特にフレームワークで使用される場合、常に 多少の混乱の元になります。残念。そこで、このような経験則があります: get_current_registry() 関数や get_current_request() 関数を使用するコードを 呼んでいるかどうか 知らない 場合、あるいはそれらを気にしていないけれど テストコードは書きたいという場合、単純にテストの setUp メソッドの 中で常に pyramid.testing.setUp() を呼び、テストの tearDown メソッド の中で常に pyramid.testing.tearDown() を呼ぶようにしてください。 テストしているアプリケーションが get_current* 関数を呼んでいなければ、 実際にはこれはまったくの無害です。

単体テストの中で Configuratorpyramid.testing API を使う

Configurator API と pyramid.testing モジュールは、単体テストの 間に使用できる多くの機能を提供します。これらの機能は現在の application registryconfiguration declaration 呼び 出しを作ります。しかし、典型的には通常実行されていた場合にコードが呼び 出すだろう「実際の」機能の代わりに、「スタブ」あるいは「ダミー」の機能 を登録します。

例えば Pyramid ビュー関数を単体テストしたいと想像しましょう。

1
2
3
4
5
6
7
from pyramid.security import has_permission
from pyramid.httpexceptions import HTTPForbidden

def view_fn(request):
    if not has_permission('edit', request.context, request):
        raise HTTPForbidden
    return {'greeting':'hello'}

単体テストの間に特別なことを何もしなければ、このビュー関数内の has_permission() の呼び出しは常に True 値 を返すでしょう。通常 Pyramid アプリケーションが開始する時、それは Configurator に対して configuration declaration 呼び出し を使用して application registry を実体化します。しかし、単体 テストからアプリケーションコードを起動する時のように、このアプリケーション レジストリが (例えば認可ポリシーを持つ configurator の初期化によって) 作成も実体化もされなければ、 Pyramid API 関数は失敗するかデフォルトの結果 を返すことが多いです。では、このビュー関数内の HTTPForbidden 例外を 上げるコードの分岐をどのようにテストすればいいでしょうか。

Pyramid が提供するテスト用の API を使えば、 main 関数によって 暗黙に行われる実際のアプリケーション設定を起動する必要なしに、ユニット テストフレームワークの下で使用するための様々なアプリケーションレジストリ の登録をシミュレートすることができます。例えば、もし上記の view_fn をテストしたければ、 (それが my.package という名のパッケージに存在 すると仮定して) テスト API を使用した unittest.TestCase を書く ことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import unittest
from pyramid import testing

class MyTest(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()

    def tearDown(self):
        testing.tearDown()

    def test_view_fn_forbidden(self):
        from pyramid.httpexceptions import HTTPForbidden
        from my.package import view_fn
        self.config.testing_securitypolicy(userid='hank',
                                           permissive=False)
        request = testing.DummyRequest()
        request.context = testing.DummyResource()
        self.assertRaises(HTTPForbidden, view_fn, request)

    def test_view_fn_allowed(self):
        from my.package import view_fn
        self.config.testing_securitypolicy(userid='hank',
                                           permissive=True)
        request = testing.DummyRequest()
        request.context = testing.DummyResource()
        response = view_fn(request)
        self.assertEqual(response, {'greeting':'hello'})

上記の例で、 unittest.TestCase から継承する MyTest テストケースを 作成しています。このテストケースが Pyramid アプリケーション内にある場合、 それは setup.py test が実行される時に見つけられます。このテストケースには 2 つのテストメソッドがあります。

最初のテストメソッド test_view_fn_forbidden は、認証ポリシーが現在 のユーザーに edit パーミッションを禁止している場合に view_fn を テストします。その3行目で testing_securitypolicy() メソッドを 使用して「ダミー」の「不許可」認可ポリシーを登録しています。このメソッド は単体テストのための特別なヘルパーメソッドです。

その後、 WebOb リクエストオブジェクト API をシミュレートする pyramid.testing.DummyRequest オブジェクトを作成します。 pyramid.testing.DummyRequest は、「実際の」 Pyramid リクエストより必要なセットアップが少ないリクエスト オブジェクトです。そして生成されたリクエストでテスト対象の関数を 呼び出します。関数が呼ばれる時、 pyramid.security.has_permission()testing_securitypolicy() を通して 登録された「ダミー」の認証ポリシー (それはアクセスを拒否します) を 呼び出します。ビュー関数が HTTPForbidden エラーを上げることを チェックします。

2つ目のテストメソッド test_view_fn_allowed は、別の場合 (認証ポリシー がアクセスを許可する場合) をテストします。この結果を得るために testing_securitypolicy() に異なる 値を渡していることに注目してください。最後に、ビュー関数が値を返すこと を検証します。

このテストが setUp メソッドの中で pyramid.testing.setUp() 関数を、 tearDown メソッドの中で pyramid.testing.tearDown() 関数を 呼び出していることに注目してください。 pyramid.testing.setUp() の 結果を単体テストクラス上に config として代入しています。これは Configurator オブジェクトであり、必要ならテスト中にすべての configurator メソッドを呼び出すことができます。テストの間に Configurator API のいずれかを使う場合は 必ずテストケースの setUp および tearDown の中でこのパターンを 使用してください; これらのメソッドは、テストが実行される度に必ず 「新しい」 application registry が使用されるようにします。

Pyramid 特有のテスト用 API 全体については pyramid.testing 章を参照してください。本章では、セキュリティポリシーを登録したり、 リソースをパスに登録したり、イベントリスナーを登録したり、ビューと ビューパーミッションを登録したりするための API と、リクエストやリソースの 「ダミー」の実装を表わすクラスについて記述します。

pyramid.config の中でドキュメント化されている、 Configuratortesting_ で始まる様々なメソッドも参照してください。

結合テストの作成

Pyramid では、テスト下のコードを実行するのに十分なコンテキスト のみを与えるために、単体テスト は典型的には「モック」や「ダミー」の 実装に依存します。

「結合テスト」は、別の種類のテストを意味します。 Pyramid の結合 テストの文脈では、テストロジックはあるコードの機能性 および Pyramid フレームワークの他の部分との結合をテストします。

Pyramid へのプラグインである Pyramid アプリケーションでは、 pyramid.config.Configurator.include() によってテストの セットアップコードに includeme 関数を含めることにより、 結合テストを作成することができます。これによって、あたかもアプリケーションが 「本当に」作動しているかのように、 Pyramid 環境全体が構築され破棄されます。 これは、テストが適切に実行するのに十分なコンテキストを持っていることを 保証する重量級の方法です。また、それは、Pyramid の他の部分とコードの結合を テストします。

ビューに対する結合テストを見せることでこれを実証してみましょう。下記の テストは、アプリケーションのパッケージ名が myapp で、アプリの中に my_view という名前の関数を含む views モジュールがあり、 my_view は完全な環境のセットアップを必要とするいくつかの値にアクセス した後にレスポンス ‘Welcome to this application’ を返す、と仮定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import unittest

from pyramid import testing

class ViewIntegrationTests(unittest.TestCase):
    def setUp(self):
        """ This sets up the application registry with the
        registrations your application declares in its ``includeme``
        function.
        """
        import myapp
        self.config = testing.setUp()
        self.config.include('myapp')

    def tearDown(self):
        """ Clear out the application registry """
        testing.tearDown()

    def test_my_view(self):
        from myapp.views import my_view
        request = testing.DummyRequest()
        result = my_view(request)
        self.assertEqual(result.status, '200 OK')
        body = result.app_iter[0]
        self.failUnless('Welcome to' in body)
        self.assertEqual(len(result.headerlist), 2)
        self.assertEqual(result.headerlist[0],
                         ('Content-Type', 'text/html; charset=UTF-8'))
        self.assertEqual(result.headerlist[1], ('Content-Length',
                                                str(len(body))))

もし回避できるなら、結合テストの作成ではなく Configurator API を使って正しい「モック」登録を セットアップする単体テストを書くべきです。単体テストはより高速に実行されます (なぜなら各テストで行うことが少ないので)。また、単体テストの結果を検証する ことは比較的簡単です。

機能テストの作成

機能テストは文字通りのアプリケーションをテストします。

下記のテストは、アプリケーションのパッケージ名が myapp で、 root URL が起動された場合 HTML ボディを返すビューがある、と仮定します。 さらに、 setup.py ファイル内に WebTest パッケージに対する tests_require 依存性を追加したと仮定します。 WebTest は Ian Bicking によって書かれた機能テストのパッケージです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import unittest

class FunctionalTests(unittest.TestCase):
    def setUp(self):
        from myapp import main
        app = main({})
        from webtest import TestApp
        self.testapp = TestApp(app)

    def test_root(self):
        res = self.testapp.get('/', status=200)
        self.failUnless('Pyramid' in res.body)

このテストが実行された場合、それぞれのテストは myapp.__init__ モジュール中の main 関数を使用して「実際の」 WSGI アプリケーションを 作成して、 WebTest を使用してその WSGI アプリケーションをラップ します。 それは self.testapp に結果を代入します。 test_root という 名前のテストでは、 root URL を起動するために testapp の get メソッドを 使用しています。その後、返された HTML の中に文字列 Pyramid が含まれて いることを検証します。

webtest.TestApp インスタンスで利用可能なメソッドに関するより詳しい 情報については、 WebTest のドキュメントを参照してください。