Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 3 GET編

# はじめに
全5回(予定なので変わる可能性あり)にかけて、`Flaskでテスト駆動開発を行うために必要なノウハウを伝授`する。
3回目の本記事では、GETパラメータを加算処理するAPIに対して、テスト用クライアントを利用して自動でテストを実行例を紹介する。

1回目 [Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 1 テスト用クライアント編](https://qiita.com/kikusui6192/items/b8a9bb8c64242a29e33b)
2回目 [Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 2 デコレータ編](https://qiita.com/kikusui6192/items/1665099685f9a05820ce)
3回目 本記事
4回目 執筆中
5回目 執筆中

# 対象読者
- FlaskでこれからWebアプリまたはAPIを開発する方
- テスト自動化の勉強をしたい方

# 概要
アプリケーションに対して複数のGETパラメータを送る方法とそれらを効率よくテストする方法を解説する。
 
# ディレクトリ構造
本記事で利用するサンプルコードは以下のようなディレクトリ構造で配置してください。

```
flask_03/
├── Dockerfile 
└── app
    ├── flask_app.py
    └── test
        └── decorators.py
        └── test.py
```
# dockerのバージョン
```
$ docker --version
Docker version 19.03.12, build 48a66213fe
```
# コードの準備
## Dockerfile
```:Dockerfile
FROM python:3.6
USER root

RUN apt update
RUN /usr/local/bin/python -m pip install --upgrade pip
RUN pip install flask==1.1.2

COPY ./app /root/

WORKDIR /root/test
```

## flask_app.py(テスト対象)
今回は、2つのGETパラメータ(`var1`と`var2`)を受け取り、加算処理を行い、その結果を返すAPIを考える。
また、GETパラメータが対応していない型(例えば、数字以外の文字列)出会った場合の例外処理も考える。

``` flask_app.py
from flask import Flask, request
app = Flask(__name__)

@app.route('/sum', methods=['GET'])
def sum():
    try:
        var1 = float(request.args.get("var1"))
        var2 = float(request.args.get("var2"))
        data = str(var1 + var2)
        return data, 200
    except Exception as e: # 例外処理
        exception_message = "Invalid Parameter"
        return exception_message, 200
    

if __name__ == '__main__':
    app.run(host="0.0.0.0",port=5000)
```

## test.py(テストコード)
今回は正常系と異常系の2種類のクラスを用意する。
また詳細は最後のセクションで説明する。

```test.py
import sys
sys.path.append('../')
import flask_app # flaskアプリケーションのファイル名のから.pyを消したもの
from decorators import get_test
import unittest

class Test_flask_app_正常系(unittest.TestCase): # 正常系試験
    def setUp(self):
        # 1.GETパラメータを設定できるようにURLの末尾に?を付け、key={キーワード引数}の形式で記述
        self.ENDPOINT    = "http://localhost:5000/{route}?var1={var1}&var2={var2}"
        self.STATUS      = "200 OK"
        self.STATUS_CODE = 200
        self.ROUTE       = "sum"
        self.data        = None

    @get_test()
    def test_sumにアクセスし計算結果を受け取れること(self):
        # 2.テストケース特有の変数をリスト内に定義
        self.var1 = [1.0, 100, -1000, 0.5, -50.5, 33.3, 0]
        self.var2 = [100, -1000, 0.5, -50.5, 33.3, 0, 1.0]
        return

class Test_flask_app_異常系(unittest.TestCase): # 異常系試験
    def setUp(self):
        self.ENDPOINT    = "http://localhost:5000/{route}?var1={var1}&var2={var2}"
        self.STATUS      = "200 OK"
        self.STATUS_CODE = 200
        self.ROUTE       = "sum"
        self.data        = None

    # 3.デコレータのパラメータを設定
    @get_test(exception_message=b"Invalid Parameter")
    def test_sumにアクセスしexceptionの処理が実行されること(self):
        # 2.テストケース特有の変数をリスト内に定義
        self.var1 = ["", "hello", "python", 100, -1000, 0.5, "", "hello", "python", None, 100]
        self.var2 = [100, -1000, 0.5, "", "hello", "python", "hello", "python", "", 100, None]
        return

if __name__ == '__main__':
    unittest.main()
```
## decorators.py(デコレータ)
test.pyで利用するデコレータ。

```decorators.py
import sys
sys.path.append('../')
import flask_app
# 1.デコレータの引数
def get_test(exception_message=None):
    def recv_func(test_func):# テスト用の関数の受け取り
        def wrapper(self):# 受け取ったテスト用の関数をデコレートする
            test_func(self)
            # 2.for文を利用して複数のGETパラメータでテストを実施
            for var1, var2 in zip(self.var1, self.var2):
                with flask_app.app.test_client() as client:
                    response = client.get(self.ENDPOINT.format(route = self.ROUTE,
                                                               var1  = var1,
                                                               var2  = var2))
                # 3.デコレータの引数を用いた分岐
                if exception_message:
                    self.data = exception_message
                else:
                    self.data = str(float(var1 + var2)).encode("UTF-8")
                assert response.data        == self.data
                assert response.status      == self.STATUS 
                assert response.status_code == self.STATUS_CODE
                response.close()
        return wrapper
    return recv_func
```
# テストを実行
[ディレクトリ構造](#ディレクトリ構造)を確認し、以下のコマンドを実行する。 

```
$ ls
Dockerfile      app
$ docker build -t pytest3 .
~ 省略 ~
$ docker run --rm -it pytest3 /usr/local/bin/python /root/test/test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.016s

OK
```

# test.py(テストコード)の解説
## 1.GETパラメータを設定できるようにURLの末尾に?を付け、key={キーワード引数}の形式で記述
`self.ENDPOINT`にURLとその末尾に?と`key={キーワード引数}`の形式の文字列を格納する。
`key={キーワード引数}`の形式にすることで、様々なGETパラメータに対するテストを行い易くなる。

## 2.テストケース特有の変数をリスト内に定義
GETパラメータをリスト内に格納することで、for文を利用し易くなる。
for文で取り出した値を、`self.ENDPOINT`の`{キーワード引数}`に埋め込むことで少ないコード量で多くのテストを行うことができる。
また今回のtest.pyでは、`Test_flask_app_正常系`のテストケースで正常に処理が行われているパラメータの組み合わせを定義し、`Test_flask_app_異常系`のテストケースで例外処理(exception)の処理に入るパラメータを設定してある。

## 3.デコレータのパラメータを設定
`Test_flask_app_異常系`のテストケースにデコレータでは、引数を定義している。
これは、デコレータ内の処理で利用することができる。
今回は、例外処理の際に格納されるエラーメッセージを引数として渡す。

# decorators.pyの解説
## 1.デコレータの引数
デコレータ内で利用する引数の初期化を行うことができる。
`exception_message=None`と初期化することで、test.py内で引数を渡さなかった場合に必ずNoneになる。
例えば`Test_flask_app_正常系`の`test_sumにアクセスし計算結果を受け取れること`では引数の渡していないため`exception_message=None`がデコレータ内に渡される。
一方で、`Test_flask_app_異常系`の`test_sumにアクセスしexceptionの処理が実行されること`では、`exception_message=b"Invalid Parameter"`を引数として渡しているため、これをデコレータ内で利用できる。

## 2.for文を利用して複数のGETパラメータでテストを実施
test.pyの各テストケース内で格納したGETパラメータをfor文を利用して取り出す。
取り出した値(`var1`と`var2`)をformatメソッドを利用してキーワード引数に埋め込み、アプリケーション側にGETパラメータを送る。
アプリケーションで処理した結果をassert文で評価することによって、テストを行う。
その後、また新たなパラメータで同様の処理を繰り返す。

## 3.デコレータの引数を用いた分岐
for文の中のif文はtest.pyから`exception_message`に値が渡された場合に実施される。
`exception_message`に値が渡された場合は、Noneで初期化していることからelse文の処理が行われる。

# まとめ
・GETパラメータをアプリケーションに送る方法を説明した。
・GETパラメータをテスト用クライアントからアプリケーションに送る際、キーワード引数とformatメソッドを組み合わせるとテストコードのコード量を圧縮できる。
・デコレータの引数を利用することで、アプリケーションの例外処理に対応し易くなる。

# 次回
次はPOSTの話を近いうちに。