以前、WebTestを使ってWSGIアプリのテストを行いました。
Pythonで、WebTestを使って、WSGIサーバを起動せずにWSGIアプリのテストをする - メモ的な思考的な
他にもテストツールがないかを探したところ、wsgi-intercept
がありました。
cdent/wsgi-intercept: Intercept socket connection to wsgi applications for testing
そこで今回はwsgi-intercept
を試してみます。
目次
環境
テスト対象のアプリ
Bootleで掲示板を作成しました。仕様のとおりです。
GET /
- フォームを表示
- Cookieにハンドル名があれば、フォームのハンドル名に表示
POST /
- フォームの入力内容を保存
- データベースを用意するのは手間だったので、pickleを利用
- ハンドル名をCookieにセット
/
ヘリダイレクト
ソースコードです。
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 = 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'
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'
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/
参考:curl コマンド 使い方メモ - Qiita
GitHubに上げました。
thinkAmi-sandbox/wsgi_intercept-sample