Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 2 デコレータ編

# はじめに
全5回(予定なので変わる可能性あり)にかけて、`Flaskでテスト駆動開発を行うために必要なノウハウを伝授`する。
2回目の本記事では、デコレータを用いてテストコードのコード量を圧縮する方法を紹介する。

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

# 対象読者
- FlaskでこれからWebアプリまたはAPIを開発する方
- テスト自動化の勉強をしたい方
- [1回目](https://qiita.com/kikusui6192/items/b8a9bb8c64242a29e33b)の記事を読んだ方

# 概要
複数のエンドポイントを有するFlaskで作成されたAPIに対するテストコードを、
デコレータを利用にすることでコード量を圧縮する。
 
# ディレクトリ構造
本記事で利用するサンプルコードは以下のようなディレクトリ構造で配置してください。

```
flask_02/
├── Dockerfile
└── app
    ├── flask_app.py
    └── test
        ├── decorators.py
        ├── test1.py
        └── test2.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(テスト対象)
3つのエンドポイントを有するFlaskアプリのコード。

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

@app.route('/hello_world')
def hello_world():
    return 'Hello, World!'

@app.route('/good_morning')
def good_morning():
    return 'Good, Morning!'

@app.route('/good_night')
def good_night():
    return 'Good, Night!'

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

## test1.py(テストコード)
flask_app.pyに対して、
3つのテストケースを実施するテストコード。

```test1.py
import sys
sys.path.append('../')
import flask_app
import unittest

class Test_flask_app_正常系(unittest.TestCase):

    def setUp(self):
        self.ENDPOINT    = "http://localhost:5000/{}"
        self.DATA        = None
        self.STATUS      = "200 OK"
        self.STATUS_CODE = 200
        self.ROUTE       = None
        
    def test_1_hello_worldにアクセスできること(self):
        # 1.テストケース特有の変数を定義
        self.DATA  = b"Hello, World!"
        self.ROUTE = "hello_world"
        
        # 2.テストケースの共通部分
        with flask_app.app.test_client() as client:
            response = client.get(self.ENDPOINT.format(self.ROUTE))
        assert response.data        == self.DATA
        assert response.status      == self.STATUS 
        assert response.status_code == self.STATUS_CODE

        return

    def test_2_good_morningにアクセスできること(self):
        # 1.テストケース特有の変数を定義
        self.DATA  = b"Good, Morning!"
        self.ROUTE = "good_morning"
       
        # 2.テストケースの共通部分
        with flask_app.app.test_client() as client:
            response = client.get(self.ENDPOINT.format(self.ROUTE))
        assert response.data        == self.DATA
        assert response.status      == self.STATUS 
        assert response.status_code == self.STATUS_CODE

        return

    def test_3_good_nightにアクセスできること(self):
        # 1.テストケース特有の変数を定義
        self.DATA  = b"Good, Night!"
        self.ROUTE = "good_night"

        # 2.テストケースの共通部分
        with flask_app.app.test_client() as client:
            response = client.get(self.ENDPOINT.format(self.ROUTE))
        
        assert response.data        == self.DATA
        assert response.status      == self.STATUS 
        assert response.status_code == self.STATUS_CODE

        return

if __name__ == '__main__':
    unittest.main()
```

## test2.py(テストコード)
flask_app.pyに対して、
3つのテストケースを実施するテストコード。
実施しているテストの内容はtest1.pyと同様であるが、
このコードではデコレータを利用している。

```test2.py
import unittest
# 1.デコレータを読み込む
from decorators import *

# クラス名は日本語でも動く
class Test_flask_app_正常系(unittest.TestCase):

    def setUp(self):
        self.ENDPOINT    = "http://localhost:5000/{}"
        self.DATA        = None
        self.STATUS      = "200 OK"
        self.STATUS_CODE = 200
        self.ROUTE       = None

    # 2.デコレータを修飾
    @get_test()
    def test_1_hello_worldにアクセスできること(self):    
        # 3.テストケース特有の変数を定義
        self.DATA  = b"Hello, World!"
        self.ROUTE = "hello_world"
        return

    # 2.デコレータを修飾
    @get_test()
    def test_2_good_morningにアクセスできること(self):
        # 3.テストケース特有の変数を定義
        self.DATA  = b"Good, Morning!"
        self.ROUTE = "good_morning"
        return

    # 2.デコレータを修飾
    @get_test()
    def test_3_good_nightにアクセスできること(self):
        # 3.テストケース特有の変数を定義
        self.DATA  = b"Good, Night!"
        self.ROUTE = "good_night"
        return

if __name__ == '__main__':
    unittest.main()
```

## decorators.py(デコレータ)
test2.pyで利用するデコレータ。

```decorators.py
import sys
sys.path.append('../')
import flask_app

# デコレータの定義
def get_test():
    # テスト用の関数の受け取り
    def recv_func(test_func):
        # 受け取ったテスト用の関数をデコレートする
        def wrapper(self):
            # 1.テストケースの呼び出し
            test_func(self)
            # 2.共通している処理の集約
            with flask_app.app.test_client() as client:
                response = client.get(self.ENDPOINT.format(self.ROUTE))
            assert response.data        == self.DATA
            assert response.status      == self.STATUS 
            assert response.status_code == self.STATUS_CODE    
        return wrapper
    return recv_func
```

# テストを実行
[ディレクトリ構造](#ディレクトリ構造)を確認し、以下のコマンドを実行する。 

```
$ ls
Dockerfile      app
$ docker build -t pytest .
~ 省略 ~
$ docker run -it pytest /usr/local/bin/python /root/test/test1.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK
$ docker run -it pytest /usr/local/bin/python /root/test/test2.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK
```

# test1.py(テストコード)の解説
## 1.テストケース特有の変数を定義
1.では各テストケースで利用する変数の初期化を行なっている。
今回は、エンドポイントとAPIの戻り値を格納している。
エンドポイントとAPIの戻り値は、テスト対象となるAPIのエンドポイントや戻り値が異なるため、
テストケースごとに異なる値で初期化を行なっている。
## 2.テストケースの共通部分
2.ではテスト用クライアントを利用して、flask_app.pyのAPIを実行し戻り値をresponseに格納する。
その後のassert文を用いてresponseに格納されてるステータスやステータスコード、戻り値を比較している。
3つのテストケースで同様の処理を行なっている。
## test1.pyの書き方のデメリット
### ①.テスト駆動開発のコストの増加
具体的には、2.で説明した各テストケースで共通している部分を集約していないため、
例え微細な修正であっても、全てのテストケースで修正を行う必要がある。
これにより、テスト駆動開発を行うにあたり、テストコードの修正に稼働がかかる可能性がある。
### ②.テストコードの保守コストが増加
例えば、このテストコードを開発した方が何らかの事情でいなくなった場合、
後任者はテストコードの量が多いため、解読に時間が掛かる可能性がある。
また、今回のtest1.pyの例では、3つのテストケースのみであるが、
商用のテストコードであるとテストケースは多くなる可能性が高い。
そのため、この書き方では保守コストの増加が高くなる可能性がある。

# decorators.py(デコレータ)とtest2.py(テストコード)の解説
test1.pyのデメリットを解決するために、
デコレータを用いてテストコードを集約する。
# decorators.py(デコレータ)の解説
## 1.テストケースの呼び出し
ネストの最上位の関数(`get_test()`)でデコレートされたテストケースを、
`test_func(self)`で実行することができる。
また、引数のselfは、対象となるテストケースのselfと同一であるため、
`setUp(self)`で定義したプロパティを引き継ぐことができる。
## 2.共通している処理の集約
wrapper内に書かれた処理は、
デコレータで修飾されたテストケースで利用することができる。
すなわち、共通して利用する処理を`wrapper(self)`内に書くことで、
コード量を圧縮することが可能となる。
今回は、テストケースで共通で利用するテスト用クライアントとassert文を用いてステータスやステータスコード、戻り値を比較している処理を共通で利用できるようにしている。

# test2.py(テストコード)の解説 
## 1.デコレータを読み込む 
decorators.pyで定義したデコレータ用の関数を全て読み込む。
## 2.デコレータを修飾
decorators.pyで定義した`get_test()`でテストケースを修飾する。
## 3.テストケース特有の変数を定義
エンドポイントとAPIの戻り値は、テストケースごとに異なるため、
デコレータに集約することはできない。
そのため、テストケース内で初期化を行なっている。

## デコレータをテストコードで利用するメリット
[test1.pyの書き方のデメリット](##test1.pyの書き方のデメリット)に書かれているデメリットを低減することが可能となる。

# まとめ
・テストコードにデコレータを用いることで、テストコードのコード量を削減する手法を紹介した。
・test2.pyの`wrapper(self)`の中身を書き換えるだけで、他のFlaskのアプリでも利用することが可能である。

# 次回
[Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 3 GET編]
(https://qiita.com/kikusui6192/items/f01573f380d5f4e73456)