BaseHTTPRequestHandler をテストする

# Overview

のっぴきならない事情で python2 だけど、 [*2to3*](https://docs.python.jp/3/library/2to3.html) かければ3でもいけると思います。

pythonでwebアプリならだいたい wsgi + リバースプロキシ で動かすであろう昨今、
今どき素の [BaseHTTPRequestHandler](https://docs.python.jp/3/library/http.server.html) なんて使ってる人いるのか謎なレベルですが、

ちょっとこれ

```py:これ
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 のモック化

似たようなこと考える人はいるらしく、[先人の知恵](https://stackoverflow.com/questions/25369068/python-how-to-unit-test-a-custom-http-request-handler)を借ります。

```python
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)
```

```py:……やったか！？
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 を閉じてしまっている](https://github.com/python/cpython/blob/2.7/Lib/SocketServer.py#L716) ため、レスポンスの検証ができません。

```py:なん…だと…
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()` の中身自体は大したことやってないので、置き換えます。まぁテストだし。

```py
In [6]: def no_finish(*args, **kwargs):
   ...:     print("no_finish", args, kwargs)
   ...:
   ...: BaseHTTPServer.BaseHTTPRequestHandler.finish = no_finish
   ...:
```

# レスポンスが取得できるようになりました

```py:大勝利の図
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レスポンスなので、

```py
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)します。

```py
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
```

```py
source = FakeSocket(resp_str)
response = HTTPResponse(source)
response.begin()
```

# `HTTPResponse`のインスタンスにできました

```py
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>
```

# テストを書く

ここまでくれば後は普通にテストを書くだけ。

```py
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リクエスト書くのが非常につらかった。
調べてみたら [このへん](https://github.com/python/cpython/blob/2.7/Lib/httplib.py#L725) をちょっとアレしてやればいけそうなので、

```py
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
```

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


おわり。