Overview
のっぴきならない事情で python2 だけど、 2to3 かければ3でもいけると思います。
pythonでwebアプリならだいたい wsgi + リバースプロキシ で動かすであろう昨今、
今どき素の BaseHTTPRequestHandler なんて使ってる人いるのか謎なレベルですが、
ちょっとこれ
import BaseHTTPServer
from StringIO import StringIO
class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
print("executing do_GET")
self.send_response(200)
self.send_header("X-Hoge", "hoge")
self.send_header("Content-type", "text/html")
self.end_headers()
html = "<html><p>hello world</p></html>"
self.wfile.write(html.encode())
のテストを書きたい事情に迫られたので、周辺情報を調べた結果。
version
$ python -V
Python 2.7.15
Request と Server のモック化
似たようなこと考える人はいるらしく、先人の知恵を借ります。
class MockRequest(object):
def makefile(self, *args, **kwargs):
return StringIO(b"GET / HTTP/1.1")
class MockServer(object):
def __init__(self, ip_port, Handler):
self.handler = Handler(MockRequest(), ip_port, self)
In [3]: server = MockServer(('0.0.0.0', 8888), MyHandler)
executing do_GET
0.0.0.0 - - [23/Jan/2019 17:51:37] "GET / HTTP/1.1" 200 -
Response が取得できない問題
リクエストを送るだけならこれでOKなんですが、
基底のハンドラがレスポンスを書き込み終わった後に StringIO を閉じてしまっている ため、レスポンスの検証ができません。
In [4]: server.handler.wfile.closed
Out[4]: True
In [5]: server.handler.wfile.getvalue()
(..snip..)
ValueError: I/O operation on closed file
finish()
を潰す
finish()
の中身自体は大したことやってないので、置き換えます。まぁテストだし。
In [6]: def no_finish(*args, **kwargs):
...: print("no_finish", args, kwargs)
...:
...: BaseHTTPServer.BaseHTTPRequestHandler.finish = no_finish
...:
レスポンスが取得できるようになりました
In [7]: server = MockServer(('0.0.0.0', 8888), MyHandler)
...:
executing do_GET
0.0.0.0 - - [23/Jan/2019 18:00:40] "GET / HTTP/1.1" 200 -
('no_finish', (<__main__.MyHandler instance at 0x1082db3b0>,), {})
In [8]: server.handler.wfile.closed
Out[8]: False
In [9]: server.handler.wfile.getvalue()
Out[9]: 'HTTP/1.0 200 OK\r\nServer: BaseHTTP/0.3 Python/2.7.15\r\nDate: Wed, 23 Jan 2019 09:00:40 GMT\r\nX-Hoge: hoge\r\nContent-type: text/html\r\n\r\n<html><p>hello world</p></html>'
レスポンスの文字列をパースする
が、このままだと素のhttpレスポンスなので、
In [10]: resp_str = server.handler.wfile.getvalue()
...: print(resp_str)
...:
HTTP/1.0 200 OK
Server: BaseHTTP/0.3 Python/2.7.15
Date: Wed, 23 Jan 2019 09:00:40 GMT
X-Hoge: hoge
Content-type: text/html
<html><p>hello world</p></html>
こいつをパースする必要があるんですが、
ここがまたひと手間必要で、[socketをモック化]
(https://stackoverflow.com/questions/24728088/python-parse-http-response-string)します。
from httplib import HTTPResponse
from StringIO import StringIO
class FakeSocket():
def __init__(self, response_str):
self._file = StringIO(response_str)
def makefile(self, *args, **kwargs):
return self._file
source = FakeSocket(resp_str)
response = HTTPResponse(source)
response.begin()
HTTPResponse
のインスタンスにできました
In [13]: print(response.status)
...: print(response.getheaders())
...: print(response.read())
...:
200
[('date', 'Wed, 23 Jan 2019 09:00:40 GMT'), ('x-hoge', 'hoge'), ('content-type', 'text/html'), ('server', 'BaseHTTP/0.3 Python/2.7.15')]
<html><p>hello world</p></html>
テストを書く
ここまでくれば後は普通にテストを書くだけ。
import pytest
def test_handler():
server = MockServer(('0.0.0.0', 8888), MyHandler)
resp_str = server.handler.wfile.getvalue()
source = FakeSocket(resp_str)
response = HTTPResponse(source)
response.begin()
headers = { x[0]: x[1] for x in response.getheaders() }
body = response.read()
assert response.status == 200
assert headers["content-type"] == 'text/html'
assert "x-hoge" in headers
assert "hello world" in body
完成図
めんどくさくなってきたのでがーっと全部1ファイルに書いちゃったけど、 gist に上げておきました。
https://gist.github.com/arc279/53b16c7a7274dab3a197f8123249cb86
追記: 素のhttpリクエスト書くのつらい問題
実際試そうと思って気付いたんですけど、素のhttpリクエスト書くのが非常につらかった。
調べてみたら このへん をちょっとアレしてやればいけそうなので、
In [4]: import httplib
...: from StringIO import StringIO
...:
In [5]:
...: class FakeSocket2:
...: def __init__(self):
...: self._file = StringIO()
...: def sendall(self, data):
...: self._file.write(data)
...:
In [6]: con = httplib.HTTPConnection("localhost:8000")
...: con.sock = FakeSocket2()
...: con
...:
Out[6]: <httplib.HTTPConnection instance at 0x105b74368>
In [7]: con.request("GET", "/index.html")
...:
In [8]: print(con.sock._file.getvalue())
GET /index.html HTTP/1.1
Host: localhost:8000
Accept-Encoding: identity
こんな感じでどうですかね。
おわり。