Pythonで、「WSGIサーバを起動せずにWSGIアプリをテストする」方法を探してみたところ、ライブラリWebTest
がありました。
Pylons/webtest: Wraps any WSGI application and makes it easy to send test requests to that application, without starting up an HTTP server.
そこで、以下を参考にして、WebTestを使ったテストコードを書いてみました。
- 公式ドキュメント
- webtest API — WebTest 2.0.28.dev0 documentation
- TestApp, TestRequest, TestResponseなどのよく使いそうなオブジェクトのメソッドや属性の解説がある
- TestApp — WebTest 2.0.28.dev0 documentation
- Form handling — WebTest 2.0.28.dev0 documentation
- webtest API — WebTest 2.0.28.dev0 documentation
- PyTest et WebTest
目次
環境
- Mac OS X 10.11.6
- Python 3.6.0
- WebTest 2.0.27
- 依存パッケージ
- BeautifulSoup4 4.5.3
- WebOb 1.7.2
- 依存パッケージ
- pytest 3.0.7
- テストランナー
Hello world的なWSGIアプリのテスト
Hello worldを表示するWSGIアプリを作成しました。
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"Hello, world."]
次にテストコードを書きます。
WebTestではTestApp
を使うことで、擬似的なHTTPリクエスト・レスポンスをテストできます。
GETのテストコードを書いてみたところ、テストをパスしました。
from webtest import TestApp import simple_wsgi_app class Test_simple_wsgi_app(object): def test_get(self): # TestAppにテスト対象のアプリケーションを渡す sut = TestApp(simple_wsgi_app.application) # getリクエストを送信 actual = sut.get('/') # ステータスコード・content-type、ボディのテスト assert actual.status_code == 200 assert actual.content_type == 'text/plain' assert actual.body == b'Hello, world.'
GETやPOSTでjinja2テンプレートを返すWSGIアプリのテスト
続いて、以前作成したWSGIアプリを使って、GETやPOSTのテストを書いてみます。bbs_wsgi_app.py
が今回使うアプリです。
wsgi_application-sample/bbs_wsgi_app.py
このアプリは、
というモノです。
import datetime import cgi import io from jinja2 import Environment, FileSystemLoader class Message(object): def __init__(self, title, handle, message): self.title = title self.handle = handle self.message = message self.created_at = datetime.datetime.now() class MyWSGIApplication(object): def __init__(self): self.messages = [] def __call__(self, environ, start_response): if environ['REQUEST_METHOD'].upper() == "POST": decoded = environ['wsgi.input'].read().decode('utf-8') header_body_list = decoded.split('\r\n') body = header_body_list[-1] encoded_body = body.encode('utf-8') with io.BytesIO(encoded_body) as bytes_body: fs = cgi.FieldStorage( fp=bytes_body, environ=environ, keep_blank_values=True, ) self.messages.append(Message( title=fs['title'].value, handle=fs['handle'].value, message=fs['message'].value, )) location = "{scheme}://{name}:{port}/".format( scheme = environ['wsgi.url_scheme'], name = environ['SERVER_NAME'], port = environ['SERVER_PORT'], ) start_response( '301 Moved Permanently', [('Location', location), ('Content-Type', 'text/plain')]) # 適当な値を返しておく return [b'1'] else: jinja2_env = Environment(loader=FileSystemLoader('./templates', encoding='utf8')) template = jinja2_env.get_template('bbs.html') html = template.render({'messages': self.messages}) start_response('200 OK', [('Content-Type', 'text/html')]) return [html.encode('utf-8')] app = MyWSGIApplication()
GETのテスト
TestAppを使ってGETのテストを書いてみます。
レスポンスボディの取得方法は2つあったため、それぞれ試してみます。
body
で、バイト文字列のレスポンスボディを取得text
で、ユニコード文字列のレスポンスボディを取得
class Test_simple_wsgi_app(object): def test_get(self): """GETのテスト""" sut = TestApp(get_post_app.app) actual = sut.get('/') assert actual.status_code == 200 assert actual.content_type == 'text/html' # bodyは、レスポンスボディをバイト文字列で取得 assert 'テスト掲示板'.encode('utf-8') in actual.body # textは、レスポンスボディをユニコード文字列で取得 assert 'テスト掲示板' in actual.text
直接POSTするテスト
続いて、直接POSTリクエストを送信するテストを書いてみます。
なお、このアプリではPOSTの後にリダイレクトしています。
リダイレクトに追随するには、follow()
を使ってリダイレクト先のレスポンスを取得します。
follow(**kw) | webtest API — WebTest 2.0.28.dev0 documentation
def test_post(self): """直接POSTのテスト""" sut = TestApp(get_post_app.app) actual = sut.post( '/', {'title': 'ハム', 'handle': 'スパム', 'message': 'メッセージ'}) assert actual.status_code == 301 assert actual.content_type == 'text/plain' assert actual.location == 'http://localhost:80/' assert actual.body == b'1' # redirectの検証には、follow()を使う redirect_response = actual.follow() assert 'ハム' in redirect_response.text assert 'スパム さん' in redirect_response.text assert 'メッセージ' in redirect_response.text
フォームのsubmitを使ってPOSTするテスト
WebTestではフォームのsubmitボタンを押すこともできるため、
- GETでフォームを取得
- フォームに入力し、submitボタンを押してPOSTする
というテストも行えます。
def test_form_post(self): """GETして、formに入力し、submitボタンを押すテスト""" sut = TestApp(get_post_app.app) # 属性formを使って、フォームの中身をセット form = sut.get('/').form form['title'] = 'ハム' form['handle'] = 'スパム' form['message'] = 'メッセージ' # submit()を使って、フォームデータをPOST actual = form.submit() assert actual.status_code == 301 assert actual.content_type == 'text/plain' assert actual.location == 'http://localhost:80/' assert actual.body == b'1' redirect_response = actual.follow() assert 'ハム' in redirect_response.text assert 'スパム さん' in redirect_response.text assert 'メッセージ' in redirect_response.text
BeautifulSoupを使ったPOSTの検証
WebTestではBeautifulSoupを使った値取得もできます。
- html | webtest API — WebTest 2.0.28.dev0 documentation
- get_text() | Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation
BeautifulSoupを使ったテストコードを書いてみます。
def test_post_with_beautifulsoup(self): """BeautifulSoupを使って検証する""" sut = TestApp(get_post_app.app) response = sut.post( '/', {'title': 'ハム', 'handle': 'スパム', 'message': 'メッセージ'}) redirect_respose = response.follow() # response.htmlで、BeautifulSoupオブジェクトを取得できる actual = redirect_respose.html title = actual.find('span', class_='title') # BeautifulSoupのget_text()で出力してみると、文字化けしていた print(title.get_text()) #=> ������������ assert '「ハム」' == actual.find('span', class_='title').get_text()
get_text()の結果が文字化けしたことにより、テストは失敗しました。
試しにレスポンスの内容を出力してみたところ、
def test_print_respose_object(self): """レスポンスオブジェクトを表示してみる""" sut = TestApp(get_post_app.app) actual = sut.get('/') print(actual) assert False """ Response: 200 OK Content-Type: text/html <html> <head> <meta charset="UTF-8"> <title>���������������������</title> </head> <body> <h1>������������������</h1> <form action="/" method="POST"> ��������������� <input type="text" name="title" size="60"><br> ������������������ <input type="text" name="handle"><br> <textarea name="message" rows="4" cols="60"></textarea> <input type="submit"> </form> <hr> </body> </html> """
日本語を表示する部分が文字化けしていました。
これが原因のようですが、今回は深く追求しません。
Bottleアプリのテスト
今まではWebフレームワークを使わないWSGIアプリをテストしました。
今度は、WebフレームワークであるBottleを使ってWSGIアプリを作成し、WebTestを使ってテストコードを書いてみます。
Bottleの公式ページにも、WebTestを使ってテストを書いている例がありました。
FUNCTIONAL TESTING BOTTLE APPLICATIONS | Recipes — Bottle 0.13-dev documentation
アプリコードは以下の通りです。
なお、フォームの入力値を取得する場合、request.forms.ge()
だと日本語が文字化けします。そのため、request.forms.getunicode()
を使います。
from bottle import Bottle, get, post, run, request, HTTPResponse from bottle import TEMPLATE_PATH, jinja2_template import datetime import json class Message(object): """Bottleのjinja2テンプレートへ値を引き渡すためのクラス""" def __init__(self, title, handle, message): self.title = title self.handle = handle self.message = message self.created_at = datetime.datetime.now() # テストコードで扱えるよう、変数appにインスタンスをセット app = Bottle() @app.get('/') def get_top(): return jinja2_template('bbs', message=None) @app.post('/') def post_top(): print(request.forms.get('handle')) message = Message( # get()だと文字化けするため、getunicode()を使う title=request.forms.getunicode('title'), handle=request.forms.getunicode('handle'), message=request.forms.getunicode('message'), ) return jinja2_template('bbs', message=message) @app.post('/json') def post_json(): json_body = request.json print(json_body) body = json.dumps({ 'title': json_body.get('title'), 'message': json_body.get('message'), 'remarks': '備考'}) r = HTTPResponse(status=200, body=body) r.set_header('Content-Type', 'application/json') return r if __name__ == "__main__": run(app, host="localhost", port=8080, debug=True, reloader=True)
また、HTMLテンプレートとしてjinja2を使います。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>テストタイトル</title> </head> <body> <h1>テスト掲示板</h1> <form action="/" method="POST"> タイトル: <input type="text" name="title" size="60"><br> ハンドル名: <input type="text" name="handle"><br> <textarea name="message" rows="4" cols="60"></textarea><br> <input type="submit"> </form> <hr> {% if message %} <p> <span class="title">「{{ message.title }}」</span> <span class="handle">{{ message.handle }} さん</span>   <span class="created_at">{{ message.created_at }}</span> </p> <p class="message">{{ message.message }}</p> <hr> {% endif %} </body> </html>
フォームからPOSTするテスト
GETでフォームを取得し、フォームにデータをセットして、submitボタンでデータをPOSTするテストを作成します。
class Test_bottle_app(object): @pytest.mark.xfail def test_form_submit(self): """GETして、formに入力し、submitボタンを押すテスト""" sut = TestApp(bottle_app.app) response = sut.get('/') form = response.form form['title'] = u'ハム'.encode('utf-8').decode('utf-8') form['handle'] = b'\xe3\x81\x82' #あ form['message'] = 'メッセージ' actual = form.submit() assert actual.status_code == 200 assert actual.content_type == 'text/html' assert 'ハム' in actual.text assert 'あ さん' in actual.text assert 'メッセージ' in actual.text
実行したところ、テストはパスしました。
また、直接POSTするコードもパスしました。
JSONをPOSTするテスト
続いてJSONをPOSTするテストを書いてみます。
フォームのときと同じだろうかと心配しましたが、最近クローズされたissueにてJSONに関する修正が入っていました。
Decoding issue for non-ASCII characters in JSON response · Issue #177 · Pylons/webtest
そこで、どうなるのかを試してみました。
def test_post_json(self): sut = TestApp(bottle_app.app) actual = sut.post_json('/json', dict(title='タイトル', message='メッセージ')) assert actual.status_code == 200 assert actual.content_type == 'application/json' assert actual.json.get('title') == 'タイトル' assert actual.json.get('message') == 'メッセージ' assert actual.json.get('remarks') == '備考'
実行したところ、テストはパスしました。