Python Tornado Testing Framework を試してみた


Unittest や Pytest などで足りるかなと思っていたけど Tornado の Testing Framework を調べてみることにした。Tornado HTTPServer を テストスクリプト内で起動して、Tornado Mock Server と言ってよいでしょうか、動かしてテストができるなど色々と便利な機能があり助かることが分かったので、早速実装テストしてみることにした。その時の備忘録としてサンプルコードをまとめておくことにした。今回試したのは2.4系なので若干情報が古かったりするかもしれがないが、基本的なことは変わらないはず。

# サンプルコード

```python:test_tornadoweb.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# 必要モジュールを import
import tornado.testing
import tornado.web
import mock
import urllib
from tornado.httpclient import HTTPRequest

# ヘルパー関数
def urlencode_request_params(**params):
    return urllib.urlencode(
        dict(
            [k, v.encode("utf-8") if isinstance(v, unicode) else v]
            for k, v in params.items()
        )
    )

def prepare_kwargs(**params):
    kwargs = dict(
        body=urlencode_request_params(**params),
    )
    return kwargs

# サンプルハンドラー
class ProfileHandler(tornado.web.RequestHandler):
    """get method を試すためのもの
    """
    def get(self):
        user_id = self.get_argument("user_id")
        if user_id != "tornadoweb":
            raise tornado.web.HTTPError(404)
        self.finish("ok")

class AccountHandler(tornado.web.RequestHandler):
    """tornado.web.authenticated デコレーターを使う場合のテストを
    するためのもの
    """
    @tornado.web.authenticated
    def get(self):
        self.finish("I'm " + self.current_user)

    @tornado.web.authenticated
    def post(self):
        bio = self.get_argument("bio")
        self.finish("{error:0, msg:{bio:%s}}" % bio)

class TestSample(tornado.testing.AsyncHTTPTestCase):
    """AsyncHTTPTestCase を継承してHTTP Serverを起動
    """
    def setUp(self):
        """super クラスで空きのポートを探し get_app に登録されている
        handler を Application として Tornado を起動している

        self.http_clientが準備されていて、self.http_client.fetch()
        を使うこともできる
        """
        super(TestSample, self).setUp()

    def get_httpserver_options(self):
        """HTTPサーバー起動時のオプションを指定することもできる
        """
        return {}

    def get_app(self):
        """アプリケーションのハンドラーのルーティングの設定を行う。
        アプリケーションの設定があれば、ここで指定することができる。
        """
        application_settings = dict()
        return tornado.web.Application([
            ("/profile", ProfileHandler),
            ("/account", AccountHandler),
            ("/account/update", AccountHandler),
        ], **application_settings)

    def test_httprequest_sample(self):
        """HTTPRequestを使って直接リクエストを投げることもできるのでお試しテスト
        """
        kwargs = dict()
        test_url = 'http://snapdish.co'
        request = HTTPRequest(test_url, **kwargs)
        self.http_client.fetch(request, self.stop, **kwargs)
        response = self.wait()
        self.assertEqual(response.code, 200)

    def test_profile(self):
        """プロフィールが正常に取得できているかのテスト
        """
        kwargs = dict(
            user_id="tornadoweb"
        )
        path = "%s?%s" % ("/profile", urlencode_request_params(**kwargs))
        response = self.fetch(path)
        self.assertEqual(response.code, 200)
        self.assertEqual(response.body, "ok")

    def test_profile_404(self):
        """プロフィールのリクエストが間違っている場合404を返してるかのテスト
        """
        kwargs = dict(
            user_id="tornadoweb?"
        )
        path = "%s?%s" % ("/profile", urlencode_request_params(**kwargs))
        response = self.fetch(path)
        self.assertEqual(response.code, 404)

    def test_account(self):
        """authenticatedのデコレーターが使われている時、mock を使って
        current_user に修正を加える必要がある"""
        with mock.patch.object(AccountHandler, 'get_current_user') as m:
            m.return_value = "tornadoweb"
            path = "/account"
            response = self.fetch(path)
        self.assertEqual(response.code, 200)
        self.assertEqual(response.body, "I'm tornadoweb")

    def test_account_update(self):
        """authenticatedのデコレーターが使われている時、mock を使って
        current_user に修正を加える必要がある"""
        with mock.patch.object(AccountHandler, 'get_current_user') as m:
            m.return_value = "tornadoweb"
            bio = "tornadoweb bio"
            params = dict(bio=bio)
            response = self.fetch("/account/update",
                                  method="POST",
                                  follow_redirects=False,
                                  body=urlencode_request_params(**params))
        self.assertEqual(response.code, 200)
        self.assertEqual(response.body, "{error:0, msg:{bio:%s}}" % bio)
```

# 備忘録
## setUp()
空きポート探して、http_client 準備して、アプケーションハンドラーをセットして、Tornado HTTPServer を起動する処理で動いている。
## get_app()
サブクラス側で実装するもので、テストしたいハンドラーとその path を指定する。アプリケーションの設定もこちらで行う。template_pathなど指定する。
## fetch(path, **kwargs)
```self.http_client.fetch``` のラッパーで中で```self.get_url(path)```をしている。```self.get_url(path)``` は、引数として渡した path を使って、このテストで起動した HTTPServer への url 生成をして request を実行してくれる。複雑なリクエストを投げたい時は、```self.http_client.fetch(request, self.stop, **kwargs)``` を使って直接リクエストを処理する。その際、response は ```self.wait()``` を使って取得する。
## tornado.web.authenticated デコレーターはサポートしていない
mockを使ってチートするか、クッキーをチートするかどちらかの方法でやるしかないみたい。HTTPRequestを使ってやることもできるので、実装次第で色々とできる。
## コマンドライン実行
モジュール名(ファイル名）を以下のように指定して実行する。

```bash
$ python -m tornado.test.runtests test_tornadoweb
```

## 参照
https://github.com/tornadoweb/tornado/tree/master/tornado/test
http://www.tornadoweb.org/en/branch2.4/testing.html