ぽよメモ

レガシーシステム考古学専攻

falconでAPIのテストを書く

falconとはpythonの軽量webAPIフレームワークで,手軽にちゃちゃっとパフォーマンスの良い(らしい)APIを書けます.手軽さで言うと正直Flaskでもそんな変わらないのではと今となっては思いますが,公式曰くFlaskやBottleと比べて高速に動作するそうです.

falconframework.org

環境

  • python==3.5.2
  • falcon==1.1.0
  • gunicorn==19.4.5

基本的にPyCharmで書いています.

APIを書く

テスト駆動開発という観点から見ればまずテストから書くべきなのですが,今回は都合上先にプログラムを書いていきます.
詳しいリファレンスは本家参照*1

ライブラリのインストール

$ pip install falcon gunicorn

gunicornは書いたAPIをwebから叩くために用います.

Hello Worldする

# sample.py

import falcon
import json


def create_json_resp(d: dict) -> str:
    return json.dumps(d, indent=2)


class SampleResource(object):
    @staticmethod
    def on_get(req, resp):
        resp.body = create_json_resp({
            "title": "hello world",
            "sentence": "This is a test program."
        })
        resp.status = falcon.HTTP_200 # デフォルトで200なので無くて良い

app = falcon.API()
app.add_route("/hello", SampleResource())

これで/helloGETするとレスポンスを返すAPIができました.動かしてみましょう.

$ gunicorn sample:app

これで127.0.0.1:8000/helloを叩くと設定したjsonが返ってくると思います.

日付を与えると曜日を返すAPI

helloだけでは寂しいので,タイトルそのままのAPIを書きます.

# sample.py

import falcon
from datetime import datetime

# 省略

class WeekResource(object):
    @staticmethod
    def on_get(req, resp, _date):
        week_data = ["月", "火", "水", "木", "金", "土", "日"]
        try:
            d = datetime.strptime(_date, "%Y-%m-%d")
            resp.body = create_json_resp({
                "input": _date,
                "week": week_data[d.weekday()]
            })
        except ValueError:
            resp.body = create_json_resp({
                "error": "ValueError",
                "detail": "invalid value",
                "input": _date
            })
            resp.status = falcon.HTTP_400

app = falcon.API()
app.add_route("/hello", SampleResource())
app.add_route("/week/{_date}", WeekResource())

これで127.0.0.1:8000/week/2016-12-25とかを叩くとレスポンスのweekにはと入っているはずです.

テストする

テストを書く

公式のリファレンスによるとunittest式の書き方と,pytest式の書き方ができるようです.今回はunittestで書いていきます.

from falcon import testing
import sample


class MyTest(testing.TestCase):
    def setUp(self):
        super(MyTest, self).setUp()
        # sample.pyからappを渡す
        self.app = sample.app


class TestMySample(MyTest):
    def test_hello(self):
        hello_resp = self.simulate_get("/hello")
        self.assertEqual(hello_resp.status_code, 200)
        self.assertEqual(hello_resp.json["title"], "hello world")

    def test_week(self):
        param = "2016-12-25"
        week_resp = self.simulate_get("/week/" + param)
        self.assertEqual(week_resp.status_code, 200)
        self.assertEqual(week_resp.json["input"], param)
        self.assertEqual(week_resp.json["week"], "日")

    def test_invalid_value(self):
        # 間違った形式のパラメータを与えて正しいエラーが返ってくるかを確認
        param = "20161225"
        week_resp = self.simulate_get("/week/" + param)
        self.assertEqual(week_resp.status_code, 400)
        self.assertEqual(week_resp.json["input"], param)
        self.assertEqual(week_resp.json["error"], "ValueError")

公式に書かれているように

class falcon.testing.TestCase(methodName='runTest')
Extends unittest to support WSGI functional testing.

なので,assertEqualなどはunittestのリファレンス*2を参照すると良いと思います.

simulate_get等のメソッドについてはfalconのリファレンスが参考になります*3

テストを実行する

僕はPyCharmで書いているので,このtest_sample.pyを走らせると自動的にunittestで走らせてくれるようです.
実行すると以下の様に表示されます.

f:id:pudding_info:20161228145140p:plain

成功する場合は何も表示されませんが,例えば曜日を計算するときのメソッドにはdatetime.weekday()datetime.isoweekday()の二種類があり,返ってくる値が異なります*4
このメソッドを間違って使用してしまったと仮定し,sample.pyの該当箇所を書き換えてもう一度テストを実行してみます.

f:id:pudding_info:20161228150556p:plain

このようにどこでfailしたのかを教えてくれます.

コンソールから叩く場合は以下の様にします.

$ python -m unittest test_sample.py

この場合も全てのテストに成功すると以下の様に

f:id:pudding_info:20161228150932p:plain

失敗するとこのようにエラーを吐いてくれます.

f:id:pudding_info:20161228151009p:plain

カバレッジも計測できますが,それは他の優良なサイトにお任せします*5

まとめ

webAPIはいちいち立ち上げて,自分でパラメータ打ち込んで,ブラウザとにらめっこして〜なんてしなくてもデバッグできますし,こうすべきでしょう.これまで手動でやってたなんてとても言えない.
テストケースはこれで十分なのかとか,むしろそっちの方が難しいので,せめてc0カバレッジくらいは…まあせいぜい「思ったように動いてくれる」ことを確かめられる,程度の気持ちでいた方が良さそうです.