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

## 概要

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

PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する
https://qiita.com/kai_kou/items/5d73de21818d1d582f00

ソースはこちら。

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

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

```sql:mysql/sqls/initialize.sql
CREATE DATABASE hoge;
CREATE DATABASE test_hoge;
use hoge;
```


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

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

単体テスト用に```pytest``` と```flask_testing``` を追加しています。

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

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


```txt: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``` でテスト用の設定を読み込み、```setUp``` と```tearDown``` でテストごとにテーブルの追加やテーブルの削除が行なわれるようにしています。

```python: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``` くらい。。。


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

```

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

```sh
> 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://qiita.com/kai_kou/items/5d73de21818d1d582f00

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