2
1

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)を行うためのチュートリアル - 3 GET編

Posted at

はじめに

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

1回目 Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 1 テスト用クライアント編
2回目 Flaskでテスト駆動開発(TDD)を行うためのチュートリアル - 2 デコレータ編
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パラメータ(var1var2)を受け取り、加算処理を行い、その結果を返す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文を利用して取り出す。
取り出した値(var1var2)をformatメソッドを利用してキーワード引数に埋め込み、アプリケーション側にGETパラメータを送る。
アプリケーションで処理した結果をassert文で評価することによって、テストを行う。
その後、また新たなパラメータで同様の処理を繰り返す。

3.デコレータの引数を用いた分岐

for文の中のif文はtest.pyからexception_messageに値が渡された場合に実施される。
exception_messageに値が渡された場合は、Noneで初期化していることからelse文の処理が行われる。

まとめ

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

次回

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

2
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?