share facebook facebook twitter menu hatena pocket slack

2018.11.05 MON

PythonのFlaskでMySQLを利用したRESTfulなAPIにpytestで単体テストを追加する

甲斐 甲

WRITTEN BY 甲斐 甲

概要

前に実装した環境でpytest を利用して単体テストができるようにしてみました。

PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://cloudpack.media/43980

ソースはこちら。

kai-kou/flask-mysql-restful-api-on-docker
https://github.com/kai-kou/flask-mysql-restful-api-on-docker

手順

上記ソースに対して、単体テストが実行できるように変更した箇所を抜粋します。

最終形は上記リポジトリのfeature/add_test ブランチに置いてます。
https://github.com/kai-kou/flask-mysql-restful-api-on-docker/tree/feature/add_test

単体テスト用のデータベースが初期化時に作成されるようにします。

mysql/sqls/initialize.sql

CREATE DATABASE hoge;
CREATE DATABASE test_hoge;
use hoge;

単体テスト用のデータベースが参照できるようにTestingConfig クラスを追加しています。

src/config.py

import os


class DevelopmentConfig:

  # SQLAlchemy
  SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{database}?charset=utf8'.format(
    **{
      'user': os.getenv('DB_USER', 'root'),
      'password': os.getenv('DB_PASSWORD', 'hoge'),
      'host': os.getenv('DB_HOST', 'db'),
      'database': os.getenv('DB_DATABASE', 'hoge'),
    })
  SQLALCHEMY_TRACK_MODIFICATIONS = False
  SQLALCHEMY_ECHO = False

class TestingConfig:

  # SQLAlchemy
  SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{database}?charset=utf8'.format(
    **{
      'user': os.getenv('DB_USER', 'root'),
      'password': os.getenv('DB_PASSWORD', 'hoge'),
      'host': os.getenv('DB_HOST', 'db'),
      'database': os.getenv('DB_DATABASE', 'test_hoge'),
    })
  SQLALCHEMY_TRACK_MODIFICATIONS = False
  SQLALCHEMY_ECHO = False


Config = DevelopmentConfig

単体テスト用にpytestflask_testing を追加しています。

flask_testing についてはあまり情報がなく、公式を参考にしました。

Flask-Testing Flask-Testing 0.3 documentation
https://flask-testing.readthedocs.io/en/latest/

requirements.txt

flask
sqlalchemy
flask-restful
flask-sqlalchemy
flask-migrate
pymysql
gunicorn
flask_marshmallow
marshmallow-sqlalchemy
pytest
flask_testing

単体テスト時にアプリやテーブルの初期化などを行うベースとなるクラスを定義します。

こちらは下記を参考にさせてもらいました。

Microservices with Docker, Flask, and React – Test Setup
https://testdriven.io/part-one-test-setup

create_app でテスト用の設定を読み込み、setUptearDown でテストごとにテーブルの追加やテーブルの削除が行なわれるようにしています。

src/tests/base.py

from flask_testing import TestCase

from src.app import app

from src.database import db, init_db


class BaseTestCase(TestCase):
  def create_app(self):
    app.config.from_object('src.config.TestingConfig')
    return app

  def setUp(self):
    self.app = self.app.test_client()
    db.create_all()
    db.session.commit()

  def tearDown(self):
    db.session.remove()
    db.drop_all()

実際のテストです。上記で定義したBaseTestCase クラスを継承しています。
実装しているリソースの各メソッドにアクセスできるかチェックしてるだけです。

flask_testing を利用すると、self.assert_200(response) などと書けて良いのですが、すべてのステータスコード分ないので、ちょっと微妙です。せめて201 くらい。。。

src/tests/test_hoge.py

from .base import BaseTestCase

import json

from src.app import app


class TestHogeListAPI(BaseTestCase):

  def test_get_hoges_no_data(self):
    response = self.app.get('/hoges')
    self.assert_200(response)
    assert(
      json.loads(response.get_data()) == {'items': []}
    )

  def test_delete_hoges_200(self):
    postPrms = {
      'name': 'hoge',
      'state': 'hoge'
    }
    response = self.app.post('/hoges',
        data=json.dumps(postPrms),
        content_type='application/json'
    )
    self.assert_status(response, 201)

    data = json.loads(response.get_data())
    id = data['id']
    response = self.app.get(f'/hoges/{id}')
    self.assert_status(response, 200)

  def test_get_hoges_one_data(self):
    postPrms = {
      'name': 'hoge',
      'state': 'hoge'
    }
    response = self.app.post('/hoges',
        data=json.dumps(postPrms),
        content_type='application/json'
    )
    self.assert_status(response, 201)

    response = self.app.get('/hoges')
    self.assert_200(response)
    data = json.loads(response.get_data())
    assert(len(data['items']) == 1)


class TestHogeAPI(BaseTestCase):

  def test_get_hoges_404(self):
    response = self.app.get('/hoges/xxx')
    self.assert_404(response)

  def test_post_hoges_201(self):
    prms = {
      'name': 'hoge',
      'state': 'hoge'
    }
    response = self.app.post('/hoges',
        data=json.dumps(prms),
        content_type='application/json'
    )
    self.assert_status(response, 201)

  def test_put_hoges_204(self):
    postPrms = {
      'name': 'hoge',
      'state': 'hoge'
    }
    response = self.app.post('/hoges',
        data=json.dumps(postPrms),
        content_type='application/json'
    )
    self.assert_status(response, 201)

    data = json.loads(response.get_data())
    id = data['id']
    putPrms = {
      'name': 'hoge2',
      'state': 'hoge2'
    }
    response = self.app.put(f'/hoges/{id}',
        data=json.dumps(putPrms),
        content_type='application/json'
    )
    self.assert_status(response, 204)

  def test_delete_hoges_204(self):
    postPrms = {
      'name': 'hoge',
      'state': 'hoge'
    }
    response = self.app.post('/hoges',
        data=json.dumps(postPrms),
        content_type='application/json'
    )
    self.assert_status(response, 201)

    data = json.loads(response.get_data())
    id = data['id']
    response = self.app.delete(f'/hoges/{id}')
    self.assert_status(response, 204)

  def test_get_hoges_200(self):
    postPrms = {
      'name': 'hoge',
      'state': 'hoge'
    }
    response = self.app.post('/hoges',
        data=json.dumps(postPrms),
        content_type='application/json'
    )
    self.assert_status(response, 201)

    data = json.loads(response.get_data())
    id = data['id']
    response = self.app.get(f'/hoges/{id}')
    self.assert_status(response, 200)

単体テストを実行してみます。コンテナ内・外どちらで実行してもおkです。

> docker-compose exec api pytest
============================= test session starts =============================
platform linux -- Python 3.6.6, pytest-3.8.2, py-1.7.0, pluggy-0.7.1
rootdir: /src, inifile:
collected 8 items

tests/test_hoge.py ........                                             [100%]

========================== 8 passed in 5.44 seconds ===========================

はい。
既存のコードに手を触れず、単体テストを追加することができました。
一度構成がまとまるとあとはスムーズに開発が進められそうです^^

参考

PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://cloudpack.media/43980

Flask-Testing Flask-Testing 0.3 documentation
https://flask-testing.readthedocs.io/en/latest/

Microservices with Docker, Flask, and React – Test Setup
https://testdriven.io/part-one-test-setup

元記事はこちら

PythonのFlaskでMySQLを利用したRESTfulなAPIにpytestで単体テストを追加する

甲斐 甲

甲斐 甲

2018/7にJOIN。 最近の好みはサーバレスです。なんでもとりあえず試します。

cloudpack

cloudpackは、Amazon EC2やAmazon S3をはじめとするAWSの各種プロダクトを利用する際の、導入・設計から運用保守を含んだフルマネージドのサービスを提供し、バックアップや24時間365日の監視/障害対応、技術的な問い合わせに対するサポートなどを行っております。
AWS上のインフラ構築およびAWSを活用したシステム開発など、案件のご相談はcloudpack.jpよりご連絡ください。