はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pythonのhttp.serverを使ってWebサーバの擬似不具合を再現するテスト用モックのようなドライバのような何か

Webクライアント側の例外処理のテストやそもそも挙動確認等で、サーバ側に多少奇妙な挙動、例えばHTTPヘッダーのある1項目を規定外のものにする、といったことをさせたいことはありませんか。

ただし、残りの99%部分は普通に動いて(いるようにみせかけて)欲しいというやつです。

サーバ側も管理範囲なら一時的に変更することが可能でしょうがそこまでやるのは本末転倒な場合も多く、これまた各種事情によりそもそもサーバ側の改変も難しい場合もしばしばあるでしょう。

さすがにカオスエンジニアリングは大掛かりすぎる、docker等活用もそれはそれで小回りがきかない感じというシチュエーションがあると思います。

ということで、私が知らないだけでイケてる方法はいくつもあるでしょうが、言語や軽めのフレームワークでほぼ標準で持っている動的Webサーバのライブラリでミニチュアサーバを立てるのが実のところ手っ取り早いというのが個人的な実感です。

特に、Pythonでは、標準ライブラリにhttp.serverというものがあります。

docs.python.org

http.serverは、リファレンスで下記のようにうたわれているものです。

警告 http.server is not recommended for production. It only implements basic security checks.

下手に高度なフレームワークなどを使ってしまうと、なんらかの安全弁が作動して奇妙な動作をさせる手間が増えてしまうこともあるでしょう。むしろ、http.serverのこの位置付けは、HTTPDの常駐デーモンから作成しなくて良いものの、それ以上は何も足さない、何も減らさないというところが、冒頭にあげたような用途にはちょうど良いように感じています。

ということで、http.serverを使った

  1. HTTPクライアントからのアクセス時に、「サーバ起動時に指定された秒数」だけ待機する(クライアントを待たせる)。
  2. N秒後に規定のコンテンツテキストを戻す。
  3. GETメソッド、POSTメソッドに対応

という、サーバプログラムの例です。

シェルのコンソールで、

python プログラム名.py 8080 固定で応答するコンテンツファイル  5

とすると、8080ポートで起動します。 また、各アクセスの際の応答までの待機秒数は5秒となります。

http.serverを使って奇妙な挙動をするサーバの例(Pythonプログラム)

from http.server import HTTPServer, BaseHTTPRequestHandler, ThreadingHTTPServer
import time
import sys


class MyHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        time.sleep(WAIT)
        self.send_response(200)
        self.send_header('Content-type', CTYPE)
        self.end_headers()
        self.wfile.write(DUMMY_RES.encode())

    def do_POST(self):
        time.sleep(WAIT)
        self.send_response(200)
        self.send_header('Content-type', CTYPE)
        self.end_headers()
        self.wfile.write(DUMMY_RES.encode())


def run(server):
    server(('localhost', LISTEN_PORT), MyHandler).serve_forever()


if __name__ == '__main__':
    LISTEN_PORT = int(sys.argv[1])
    CTYPE = 'application/json'
    DUMMY_RES = open(sys.argv[2], 'r', encoding='utf-8').read()
    WAIT = int(sys.argv[3])
    # run(HTTPServer)
    run(ThreadingHTTPServer)

上記のプログラム例の補足です。 末尾のrunという関数呼び出し箇所あたりに注目ください。

http.server.HTTPServer、http.server.ThreadingHTTPServerでいうと、後者を使っています。 http.server.HTTPServerはコメントアウトしている方で、シングルスレッドで動作する*1ので、複数クライアントからのアクセスの場合には、サーバ側の応答が直列化される、より奇妙な挙動を再現しやすくなります。

<<この記事おわり>>

ちなみに、http.serverについては、少し前に自分用まとめをしてします。

itdepends.hateblo.jp

*1:本来の話の流れでは、ThreadingHTTPServerがマルチスレッドで動作する高度なバージョンですが、今回は逆の視点で捉えます。