PythonのWebフレームワークflaskによるWebAPI開発とユニットテスト

API開発とユニットテスト

はじめに

今回は、PythonのWebフレームワークflaskによるWebAPI開発とユニットテストについて書きます。
対象はバックエンドサーバーのユニットテストで、WebUIのシナリオテストは含みません。

flaskはPythonにおけるWebフレームワークでも代表的なOSSです。
同様のOSSとしてはDjangoがあります。Djangoの方がデフォルトでリッチな機能がありますが、サクッとプロトタイピングするならflaskの方がお手軽です。
一方で、flaskのプロトタイプの規模が大きくなって来た場合、サービスの信頼性を確保するためにユニットテストは必須になってきます。今回の記事はそういったケースで大変参考になると思います。

ユニットテストとは、コンピュータープログラミングにおける一般的なテスト方法であり、メソッドやクラス単位の動作保証のために作成します。テスト単位を細かく分けることで、サービスの拡張や変更を行った際にエラーやバグを迅速に検知することができます。

WebAPIにおいてもユニットテストの概念は存在し、サービスにおいて重要な要素です。
また、テスト駆動開発という言葉もありますが、大事なことはテストがきちんとテストの役割を果たせるようにサービス自体の設計もしっかりと整えてやることです。サービスを適切なクラスやメソッドで設計しなければ、ユニットテストはその役割を十分に果たすことはできません。なお、今回の記事ではサービスの適切な設計については扱いません。

  1. はじめに
  2. flaskにおけるWebAPI開発について
  3. flaskにおけるユニットテストのやり方
  4. おわりに

flaskにおけるWebAPI開発について

前述の通り、サクッとプロトタイピングするならPythonのflaskはお手軽です。

flaskには「jinja2」というテンプレートエンジンが内包されており、簡単にWebUIを伴うサービスを作ることができます。また、プラグインも充実しており、プラグインを組み合わせることでDjangoで出来ることはだいたい出来ます。

flaskにおけるWebAPI開発について、便利なプラグインを紹介します。「flask-restplus」と「flask-sqlalchemy」です。

flask-restplusは、flaskにおけるWebAPI開発をDjangoライクにすることができるプラグインです。例えば下記のように、クラスに対してエンドポイントを設定し、getpostのメソッドでそれぞれGETPOSTのリクエストを受け取ることができます。

【python】

from flask import Flask
from flask_restplus import Resource, Api
app = Flask(__name__)
api = Api(app)
@api.route('/hello')
class HelloWorld(Resource):
    def get(self):
        return {'hello': 'world'}
if __name__ == '__main__':
    app.run(debug=True)

また、flask-restplusでWebAPIを開発すると、前回紹介したSwagger UIが使えます。flask-restplusで入出力のSerializerを書くことで、自動的にSwagger specに変換してSwagger UIに反映してくれます。

flask-sqlalchemyはDB処理を担当する、いわゆるORMです。flaskのセッションと連動するので、セッション管理をflaskとflask-sqlalchemyに任せることができます。flask-sqlalchemyは(以下は非構造的な悪い例ですが)以下のように使います。下記を実行すると、sqliteでuserというDBが自動的に作られます。

【python】

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    def __repr__(self):
        return '' % self.username
db.create_all()

flaskにおけるユニットテストのやり方

flaskには公式ページにテスト方法についての記載があります。下記に示すとおり、エンドポイントを指定してサンプルデータを流しこみ、レスポンスが想定どおりかをテストします。

【python】

import os
import tempfile
import pytest
from flaskr import flaskr
@pytest.fixture
def client():
    db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
    flaskr.app.config['TESTING'] = True
    client = flaskr.app.test_client()
    with flaskr.app.app_context():
        flaskr.init_db()
    yield client
    os.close(db_fd)
    os.unlink(flaskr.app.config['DATABASE'])
def test_empty_db(client):
    """Start with a blank database."""
    rv = client.get('/')
    assert b'No entries here so far' in rv.data

テストは以下のコマンドで実行します。

【bash】

# unittest
$ python -m unittest discover
# pytest
$ pytest

バックエンドでDBを使う場合は注意が必要です。当然ながら本番サービスのDBを使うことはありえませんので、テストDBに接続してください。

多少ハマるポイントとしては、ユニットテストにおけるDBアクセスです。システムが大規模化してくると、ユニットテストも増えます。ここで、テストごとに「DB作成→テスト用データの流し込み→テスト→DB削除」を行うと、テストの総実行時間が激増します。
そこで、Python標準のUnitTestのsetUpClasstearDownClassを使って、テストクラス(ユニットテストの集合)起動時に「DB作成→テスト用データの流し込み」を実行し、終了時に「DB削除」を実行するようにします。サンプルコードを後ほど示します。

このとき注意することは、ユニットテスト同士の干渉についてです。
例えば、テストクラスが複数ありそれらが並列で動作する場合、あるテストクラスが終了したときにDBが削除され、残りのテストクラスのすべてはDBアクセスが出来ない状況になります。その結果、テストは失敗となります。

また、ユニットテストによってサービスがDBエントリの追加や削除をするでしょうから、セッション管理もしっかりやる必要があります。
Pythonでは、ユニットテストにおけるテスト順序はテストクラスの名前やテストメソッドの名前でソートされて実行されます。
そのため、テスト順序をコントロールすることが非常に面倒です。そもそも、ユニットテストの本来の意味からしても、テスト順序をコントロールすることはナンセンスです。

そこで、flaskでユニットテストをやる上で役立つプラグインとして「flask-testing」を紹介します。
flask-testingは、flask-sqlalchemyも対象にしたユニットテスト用のプラグインです。下記のように使います。

【python】

from flask_testing import TestCase
from myapp import create_app, db
class MyTest(TestCase):
    SQLALCHEMY_DATABASE_URI = "sqlite://"
    TESTING = True
    def create_app(self):
        # pass in test configuration
        return create_app(self)
    def setUp(self):
        db.create_all()
    def tearDown(self):
        db.session.remove()
        db.drop_all()
class SomeTest(MyTest):
    def test_something(self):
        user = User()
        db.session.add(user)
        db.session.commit()
        # this works
        assert user in db.session
        response = self.client.get("/")
        # this raises an AssertionError
        assert user in db.session

setUpClasstearDownClassを使うときは下記のようにします。

【python】

    @classmethod
    def setUpClass(cls):
        app = create_app()
        with app.app_context():
            db.create_all()
    @classmethod
    def tearDownClass(cls):
        app = create_app()
        with app.app_context():
            db.session.remove()
            db.drop_all()

flask-testingの良い点は、flask-testingがapp_contextを管理してくれる点です。
テストクラスが並列で動いていても、他のテストクラスがDBを使っている場合はDBの削除を行いません。セッションも管理してくれます。
ユーザーが気にすべきことは、複数のユニットテストで同じエントリを操作(追加、更新、削除)しないようにユニットテストを設計することだけです。

全てのAPIエンドポイントに対してユニットテストを書くことができれば、最低限のユニットテストは実装できたと言えます。
ユニットテストの作成は大変な作業ですが、将来に渡ってのメンテナンス性の向上につながるので確実に作成しましょう。テストコードの参考になりそうなものとして、私がコミットしているOSSのDruckerのリンクを貼っておきます。

おわりに

今回は、PythonのWebフレームワークflaskによるWebAPI開発とユニットテストついて書きました。flaskはサクッとプロトタイピングできるのが特徴ですが、その分テストがおろそかになりがちです。プログラムの規模が大きくなってきたときに、今回の記事は大変参考になると思います。