2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-15

はじめに

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

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

対象読者

  • FlaskでこれからWebアプリまたはAPIを開発する方
  • テスト自動化の勉強をしたい方
  • 1回目の記事を読んだ方

概要

複数のエンドポイントを有する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の書き方のデメリットに書かれているデメリットを低減することが可能となる。

まとめ

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

次回

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

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?