LoginSignup
2
0

More than 5 years have passed since last update.

BaseHTTPRequestHandler をテストする

Last updated at Posted at 2019-01-23

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をモック化します。

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

こんな感じでどうですかね。

おわり。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0