以前、WebTestを使ってWSGIアプリのテストを行いました。
Pythonで、WebTestを使って、WSGIサーバを起動せずにWSGIアプリのテストをする - メモ的な思考的な
他にもテストツールがないかを探したところ、wsgi-intercept
がありました。
cdent/wsgi-intercept: Intercept socket connection to wsgi applications for testing
そこで今回はwsgi-intercept
を試してみます。
目次
環境
テスト対象のアプリ
Bootleで掲示板を作成しました。仕様のとおりです。
ソースコードです。
# -*- coding: utf-8 -*- import datetime import pickle from pathlib import Path import bottle from bottle import Bottle, run, redirect, request, response, jinja2_template class Message(object): def __init__(self, title, handle, message): self.title = title self.handle = handle self.message = message self.created_at = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S') # テストコードで扱えるよう、変数appにインスタンスをセット app = Bottle() @app.get('/') def get_form(): handle = request.cookies.getunicode('handle', default='') messages = read_messages() return jinja2_template('bbs.html', handle=handle, messages=messages) @app.post('/foo') def post_foo(): return "Hello World!" @app.post('/') def post_form(): response.set_cookie('handle', request.forms.get('handle')) message = Message( title=request.forms.getunicode('title'), handle=request.forms.getunicode('handle'), message=request.forms.getunicode('message'), ) messages = read_messages() messages.append(message) with open('bbs.pickle', mode='wb') as f: pickle.dump(messages, f) redirect('/') @app.get('/delete_cookie') def delete_cookie(): response.delete_cookie('handle') redirect('/') def read_messages(): if Path('bbs.pickle').exists(): with open('bbs.pickle', mode='rb') as f: return pickle.load(f) return [] if __name__ == "__main__": run(app, host="localhost", port=8080, debug=True, reloader=True)
テンプレートです。
<html> <head> <meta charset="UTF-8"> <title>テスト掲示板 | bottle app</title> </head> <body> <h1>テスト掲示板</h1> <form action="/" method="POST"> <div> <label for="title">タイトル</label> <input type="text" name="title" size="60"> </div> <div> <label for="handle">ハンドル</label> <input type="text" name="handle" value="{{handle}}"> </div> <div> <label for="message">メッセージ</label> <textarea name="message" rows="4" cols="60"></textarea> </div> <div> <input type="submit"> <a href="/delete_cookie">Cookie削除</a> </div> </form> <hr> {% for m in messages %} <p> 「{{ m.title }}」 {{ m.handle }} さん   {{ m.created_at }} </p> <p>{{ m.message }}</p> <hr> {% endfor %} </body> </html>
wsgi-interceptを使ったテストコード
wsgi-interceptでは、ライブラリ
- urllib2
- urllib.request
- httplib
- http.client
- httplib2
- requests
- urllib3
のいずれかを併用してテストコードを書きます。
今回は扱いやすいrequests
を使ってテストコードを書きます。
Requests: HTTP for Humans — Requests 2.13.0 documentation
テストの準備
テンプレートディレクトリを認識させる
wsgi-interceptでBottleをテストする場合、デフォルトではテンプレートディレクトリを認識しません。
そのため、
current_dir = os.path.abspath(os.path.dirname(__file__)) template_dir = os.path.join(current_dir, 'bbs_app/views') bottle.TEMPLATE_PATH.insert(0, template_dir)
のようにして、テンプレートディレクトリを認識させる必要があります。
Frequently Asked Questions — Bottle 0.13-dev documentation
Bottleアプリの指定について
wsgi-interceptでは、from bbs_app.bbs import app
してapp
を利用しようとしてもうまく動作しません。
そのため、
def get_app(self): return app
のように、importしたBottleアプリを返す関数やメソッドを用意し、それをwsgi-interceptを使う時に渡してあげる必要があります。
GETのテスト
ドキュメントに従い、以下のテストコードを書きます。
with RequestsInterceptor(self.get_app, host='localhost', port=8080) as url: actual = requests.get(url) assert actual.status_code == 200 assert actual.headers['content-type'] == 'text/html; charset=UTF-8' assert 'テスト掲示板' in actual.text
テストを実行すると、wsgi-interceptがrequest.get()をうまいこと処理し、レスポンス(今回はactual)が返ってきます。
あとは、その値を検証すれば良いです。
POSTのテスト(add_wsgi_intercept利用版)
上記ではwith
を利用していましたが、ドキュメントにもある通り、add_wsgi_intercept()などを使ってもテストできます。
host = 'localhost' port = 8081 url = f'http://{host}:{port}' requests_intercept.install() add_wsgi_intercept(host, port, self.get_app) form = { 'title': 'タイトル1', 'handle': 'ハンドル名1', 'message': 'メッセージ1', } actual = requests.post(url, data=form) assert actual.status_code == 200 assert actual.headers['content-type'] == 'text/html; charset=UTF-8' assert 'タイトル1' in actual.text assert 'ハンドル名1' in actual.text assert 'メッセージ1' in actual.text requests_intercept.uninstall()
install()やuninstall()など、色々と手間がかかるため、withの方が良さそうです。
POSTとリダイレクトのテスト(RequestsInterceptor利用版)
Webアプリに対してrequestsを使うときと同様の書き方で、POSTやリダイレクトのテスト・検証が行えます。
form = { 'title': 'タイトル2', 'handle': 'ハンドル名2', 'message': 'メッセージ2', } with RequestsInterceptor(self.get_app, host='localhost', port=8082) as url: # リダイレクト付 actual = requests.post(url, data=form) # リダイレクト先の検証 assert actual.status_code == 200 assert actual.headers['content-type'] == 'text/html; charset=UTF-8' assert 'タイトル2' in actual.text assert 'ハンドル名2' in actual.text assert 'メッセージ2' in actual.text # リダイレクト元の検証 assert len(actual.history) == 1 history = actual.history[0] assert history.status_code == 302 assert history.headers['content-type'] == 'text/html; charset=UTF-8' # bottleでredirect()を使った場合、bodyは''になる assert history.text == ''
また、requestsでは、post()メソッドの引数にてallow_redirects=False
とすれば、リダイレクトしなくなります。
Developer Interface — Requests 2.13.0 documentation
リダイレクトなしの場合のテストコードです。
form = { 'title': 'タイトル3', 'handle': 'ハンドル名3', 'message': 'メッセージ3', } with RequestsInterceptor(self.get_app, host='localhost', port=8082) as url: # リダイレクトさせない actual = requests.post(url, data=form, allow_redirects=False) assert len(actual.history) == 0 assert actual.status_code == 302 assert actual.headers['content-type'] == 'text/html; charset=UTF-8' # bottleでredirect()を使った場合、bodyは''になる assert actual.text == ''
その他
Bottleアプリではset_cookie()
を使っているため、POST時にhandleを設定しないと、TypeError: Secret key missing for non-string Cookie
というエラーになります。
Python Bottle - TypeError: Secret key missing for non-string Cookie. | BitmapCake!
wsgi-interceptでもエラーを確認できるのですが、エラー出力内容がPython2とPython3で異なりました。
Python2は上記のエラー出力のみですが、Python3では上記のほか、
- TypeError: getresponse() got an unexpected keyword argument ‘buffering’
- TypeError: a bytes-like object is required, not ‘str’
- wsgi_intercept.WSGIAppError: TypeError(“a bytes-like object is required, not ‘str’”,) at path/to/env/bin/bottle.py:972
が混ざっていました。特に、本当のエラーのSecret key missing
が途中に混ざっており、エラー解析しづらい印象でした。
そのため、こんな感じでcurlも併用して原因の調査をしました。
curl -XPOST --data 'title=bar&handle=baz' http://localhost:8080/