パート15: テストされた詩

はじめに

Twisted を探検するなかでたくさんのコードを書いてきましたが、これまではある重要なことを避けてきました。テストです。 Python 標準の unittest パッケージのような同期フレームワークを使って 非同期コードをテストできることを不思議に思うかもしれませんね。 短期的な答えとしては、できません、となります。 ここまでで分かったように、同期と非同期のコードを混在させることはできません。少なくとも普通は。

幸いにも、Twisted は trial と呼ばれる独自のテストフレームワークを持っています。 これは非同期コードのテストをサポートしています (同期のコードにも使えます) 。

ここからはあなたが unittest や似たようなテストフレームワークの基本的な仕組みに慣れていると仮定します。 特定の親クラス (TestCase のような名前です) を持つクラスを定義してテストを作成する方法です。 “test” という単語から始まるそれぞれのメソッドがひとつのテストとみなされます。 フレームワークが全てのテストを発見してくれて、オプションである setUptearDown ステップをはさみながら個別のテストを順番に実行してくれます。 そして最後に結果を報告します。

tests/test_poetry.py にいくつかのテスト例があります。 すべてのテスト例が自己完結することを保証するために (PYTHONPATH の設定について心配する必要はありません)、必要なコードの全てをテストモジュールにコピーしておきました。 もちろん、普通は、テストしたいモジュールをインポートすることになるでしょう。

この例では、テストサーバから詩を取得するためにクライアントを使うことによって、詩のクライアントとサーバの両方をテストしています。 テスト用に詩のサーバを提供するためには、テストケースにおいて setUp メソッドを実装します。

class PoetryTestCase(TestCase):

    def setUp(self):
        factory = PoetryServerFactory(TEST_POEM)
        from twisted.internet import reactor
        self.port = reactor.listenTCP(0, factory, interface="127.0.0.1")
        self.portnum = self.port.getHost().port

setUp メソッドはテスト用の詩を与えて詩のサーバを生成し、ランダムな空いているポートで待ち受けます。 もし必要なら、実際のテストが利用できるようにそのポート番号を使わないようにします。 もちろん、テストが終わったら tearDown の中でテストサーバを安全に終了させます。

def tearDown(self):
    port, self.port = self.port, None
    return port.stopListening()

これで最初のテスト (test_client) ができます。 ここでは、 テストサーバから詩を取得して期待した詩であるかを検証するために get_poetry を使います。

def test_client(self):
    """The correct poem is returned by get_poetry."""
    d = get_poetry('127.0.0.1', self.portnum)

    def got_poem(poem):
        self.assertEquals(poem, TEST_POEM)

    d.addCallback(got_poem)

    return d

ここでのテストは遅延オブジェクトを返すことに注意してください。 trial を使う場合はそれぞれのテストメソッドはコールバックとして動作します。 このことは、reactor が動いていてテストの一部として非同期操作を実行できることを意味します。 テストが非同期であるとフレームワークに教えるだけでよく、いつもの Twisted のやりかたに沿うだけです。 単に遅延オブジェクトを返すのです。

trial フレームワークは、 tearDown メソッドを呼び出す前に遅延オブジェクトが発火するのを待ちます。 そして、遅延オブジェクトが失敗したらテストが失敗したものとします。 (最後のコールバックとエラー用コールバックのペアが失敗したときです。) 遅延オブジェクトが発火するまでにあまりに長時間が経過した場合もテストは失敗とみまします。デフォルトでは2分です。 テストが終わったということは、遅延オブジェクトが発火されたことを意味しますので、コールバックも実行され、テストメソッドである assertEquals が実行されたわけです。

ふたつ目のテストは test_failure で、これは get_poetry がサーバに接続できなければ適切な方法で失敗することを検証します。

def test_failure(self):
    """The correct failure is returned by get_poetry when
    connecting to a port with no server."""
    d = get_poetry('127.0.0.1', -1)
    return self.assertFailure(d, ConnectionRefusedError)

ここでは、無効なポートに接続してみて、 trial が提供する assertFailure を使ってみることにします。 このメソッドは assertRaises メソッドのようなものですが、非同期コード用です。 ある例外を与えられたときに、ある遅延オブジェクトが失敗したら成功、そうでなければ失敗する遅延オブジェクトを返します。

trial スクリプトを使って、次のようにしてテストを動かすことができます。

trial tests/test_poetry.py

それぞれのテストケースを表示しながら、いくつかの出力を確認できるでしょう。 OK はテストが成功したことを意味します。

議論

trialunittest にとてもよく似てますので、基本的な API を使ってテストを書き始めることがとても簡単です。 テストが非同期のコードを使うなら遅延オブジェクトを返すだけで、残りは trial が良きようにやってくれます。 必要であれば、 setUptearDown メソッドから遅延オブジェクトを返すこともできます。

テストにおけるログメッセージは trial が自動的に作成する _trial_temp という名前のディレクトリの中のファイルに収集されるでしょう。 画面に表示されるエラーに加えて、失敗したテストをデバッグする始めの一歩としてもログは役に立ちます。

図33はテストが実行される様子を表しています。

_images/p15_test-1.png

図33: trial テストが実行される様子

以前に似たようなフレームワークを使ったことがあれば、テストに関係する全てのメソッドが遅延オブジェクトを返すかもしれないこと以外は見慣れたモデルでしょう。

trial フレームワークは、「非同期に処理が進むこと」がプログラム全体に渡って及ぼす変化をうまく表しています。 テスト (もしくは関数でもメソッドでも) が非同期であるためには、次の条件を満たさなくてはなりません。

  1. ブロックしません。そして普通は、

  2. 遅延オブジェクトを返します。

しかしこのことは、こうした関数を呼び出すものは全て、遅延オブジェクトを受け付ける準備ができていなければなりませんし、ブロックしてはいけない (それゆえに、遅延オブジェクトを返すことになるでしょう)、ということを意味します。 これはどんどん進んでいきます。 したがって、 遅延オブジェクトを返す非同期なテストを扱える trial のようなフレームワークが必要なのです。

まとめ

ユニットテストに関してはこんなところにしておきましょう。 Twisted のコードにおいてユニットテストをどのように記述すべきかの例を探すことになったら、Twisted そのものを見るに越したことはありません。 Twisted フレームワークには、毎回のリリース時に付け加えられている、非常に大規模なユニットテストがあります。 これらのテストは、コードベースに受け入れられる前にコードレビューを通して Twisted の専門家によって精査されていますので、 Twisted のコードを正しいやり方でテストする素晴らしい例となるでしょう。

パート16: Twisted をデーモン化する” では、詩のサーバをきちんとしたデーモンにするために Twisted のユーティリティを使うことにしましょう。

おすすめの練習問題

  1. テストのひとつが失敗するように変更してみましょう。また、出力を確認するために trial をもう一度動かしてみてください。

  2. オンラインの trial documentation を読んでみましょう。

  3. この連載で作成してきた詩のサービスに対するテストを書いてみましょう。

  4. Twisted にある テストのいくつか をのぞいてみましょう。