Flaskでユニットテストを行う方法

紹介

テストは、ソフトウェア開発プロセスの一環であり、コードが期待通りの動作を行い、缺陷のない状態であることを保証するために不可欠です。Pythonでは、pytestという人気のあるテストフレームワークがあり、標準的なunit testモジュールより多くの利点を提供しています。unit testはPythonの標準ライブラリの一部である内蔵のテストフレームワークです。pytestには、より簡潔な構文、より良い出力、強力なフィクション、豊富なプラグイン生態系が含まれます。このチュートリアルでは、Flaskアプリケーションの設定、pytestフィクションの統合、pytestを使用したユニットテストの書き方を解説します。

前提条件

開始前に以下のものが必要です。

  • Ubuntuを実行しているサーバーと、sudo権限を持った非rootユーザー。これらを設定する方法についてのガイドを参照するには、このリストからあなたのディストリビューションに基づいて初期サーバー設定ガイドを従ってください。Ubuntuのサポートされているバージョンで作業を行うことを確認してください。
  • Linuxのコマンドラインを熟悉していること。Linuxコマンドラインの入門を参照してください。

  • Pythonプログラミングの基本理解と、Pythonのpytestテストフレームワークについての理解。PyTest Pythonテストフレームワークのマニュアルを参照してpytestについて学びましょう。

  • UbuntuシステムにPython 3.7または新しい版がインストールされていること。PythonスクリプトをUbuntu上で実行する方法を学ぶには、How to run a Python script on Ubuntuという私のチュートリアルを参照することができます。

Why pytest is a Better Alternative to unittest

pytestは、組み込みのunittestフレームワークに比べていくつかの利点を提供します。

  • Pytestでは、より少ないステータスコードを書かないで、unittestが要求するより多くの文脈を含むメソッドを避けて、単純なassert语句を使用してテストを書くことができます。

  • Pytestは、より詳細で読みやすい出力を提供し、テストが失敗した場所と理由を特定しやすくします。

  • Pytestのフィクスチャは、unittestのsetUptearDownメソッドよりも柔軟で再利用可能なテスト設定を可能にします。

  • Pytestは、複数の入力セットで同じテスト関数を簡単に実行でき、これはunittestではそれほど簡単ではありません。

  • Pytestには、コードカバレッジツールから並列テスト実行まで、その機能を拡張する豊富なプラグインが揃っています。

  • Pytestは、その命名規則に一致するテストファイルと関数を自動的に検出し、テストスイートの管理における時間と労力を節約します。

利点があるため、pytestは、现代的なPythonテストにおいて一般的な選択肢となっています。今回は、Flaskアプリケーションを設定し、pytestを使用してユニットテストを書いてみましょう。

ステップ1 – 環境の設定

Ubuntu 24.04にはデフォルトでPython 3が含まれています。ターミナルを開いて、以下のコマンドを実行してPython 3のインストールを確認してください。

root@ubuntu:~# python3 --version
Python 3.12.3

Python 3が既に machine 上にインストールされている場合、上記のコマンドはPython 3の現在のバージョンを返します。インストールされていない場合、以下のコマンドを実行しPython 3のインストールを行います。

root@ubuntu:~# sudo apt install python3

次に、pipパッケージインストーラを system 上にインストールします。

root@ubuntu:~# sudo apt install python3-pip

一旦pipがインストールされたら、Flaskをインストールしましょう。

ステップ2 – Flaskアプリケーションの作成

まず、簡単なFlaskアプリケーションを作成しましょう。プロジェクト用の新しいディレクトリを作成し、その中に移動します。

root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app

今は、依存関係を管理するために仮想環境を作成して有効にすることにします。

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate

Flaskをpipを使用してインストールします。

root@ubuntu:~# pip install Flask

次に、簡単なFlaskアプリケーションを作成しましょう。新しいapp.pyという名前のファイルを作成し、以下のコードを追加してください。

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)

このアプリケーションには以下の3つのルーティングがあります:

  • /: 簡単な「Hello, Flask!」メッセージを返します。
  • /about: 簡単な「これはAboutページです」メッセージを返します。
  • /multiply/<int:x>/<int:y>: 2つの整数を掛け合わせて結果を返します。

アプリケーションを実行するために、以下のコマンドを実行してください。

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

上の出力から、サーバーがhttp://127.0.0.1で動いていて、ポート5000をリスニングしていることが気づけます。別のUbuntuコンソールを開いて、以下のcurlコマンドを順番に実行してください。

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

これらのGETリクエストが何を行うのかを理解しましょう:

  1. curl http://127.0.0.1:5000/:
    これは、私のFlaskアプリのルートルート(‘/’)にGETリクエストを送信します。サーバーは、「Hello, Flask!」というメッセージを含むJSONオブジェクトを返信し、私のホームルートの基本機能を示します。

  2. curl http://127.0.0.1:5000/about:
    これは、/aboutルートにGETリクエストを送信します。サーバーは、「This is the About page」というメッセージを含むJSONオブジェクトを返信し、私のルートが正しく機能していることを示します。

  3. curl http://127.0.0.1:5000/multiply/10/20:
    これは、/multiplyルートに2つのパラメータ(10と20)を持つGETリクエストを送信します。サーバーはこれらの数字を乗算し、結果(200)を含むJSONオブジェクトを返信します。これは、私の乘算ルートがURLパラメータを正しく処理し、計算を行えることを示します。

これらのGETリクエストは、FlaskアプリケーションのAPIエンドポイントとやりとりすることができ、データを変更せずにサーバーから情報を取得したり、アクションをトリガーしたりします。これらはデータを取得し、端末機能をテストし、ルートが期待通りに応答することを確認するために便利です。

次に、これらの各GETリクエストを実際に見てみましょう。

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

Step 3 – Installing pytest and Writing Your First Test

基本的なFlaskアプリケーションを作成した後は、pytestをインストールし、最初のテストを書いてみましょう。

pytestpipでインストールします。

root@ubuntu:~# pip install pytest

テスト用のdirectoryを作成し、テストファイルを保存します。

root@ubuntu:~# mkdir tests

今、新しいファイルtest_app.pyを作成し、以下のコードを追加します。

test_app.py
# sysモジュールをインポートしてPythonの运行時環境を変更する
import sys
# osモジュールをインポートして操作系统とやりとりする
import os

# 親ディレクトリをsys.pathに追加する
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# 主要なアプリファイルからFlaskアプリインスタンスをインポートする
from app import app 
# pytestをインポートしてテストを書くと実行する
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

このテストファイル内の関数を簡単に説明しましょう。

  1. @pytest.fixture def client():
    これはpytestのフィクスチャーで、Flaskアプリのテストクライアントを作成します。これは実際のサーバーを起動せずにアプリにリクエストを送信できるクライアントを作成するためにapp.test_client()メソッドを使用します。yieldステートメントは、テストにおいてクライアントを使用した後、各テストごとに適切にクローズすることができます。

  2. def test_home(client):
    この関数は、アプリのホームルート(/)をテストします。テストクライアントを使用してルートにGETリクエストを送信し、レスポンスステータスコードが200(OK)であり、JSONレスポンスが期待されるメッセージに一致することをアサートします。

  3. def test_about(client):
    test_homeと似ていますが、この関数はアプリのaboutルート(/about)をテストします。200ステータスコードを確認し、JSONレスポンスコンテンツを確認します。

  4. def test_multiply(client):
    この関数は有効な入力での乘法ルート(/multiply/3/4)をテストします。ステータスコードが200であり、JSONレスポンスが乘法の正しい結果を含んでいることを確認します。

  5. def test_multiply_invalid_input(client):
    この関数は、無効な入力(multiply/three/four)を使用して乗算ルートをテストします。ルートが文字列入力を必要な整数パラメータに一致させることができない場合の期待される動作であるステータスコード404(Not Found)を確認します。

  6. def test_non_existent_route(client):
    この関数は、存在しないルートにアクセスしたときのアプリの動作をテストします。Flaskアプリで定義されていない/non-existentにGETリクエストを送信します。テストは、レスポンスのステータスコードが404(Not Found)であることを確認し、未定義のルートへのリクエストをアプリが正しく処理していることを保証します。

これらのテストは、Flaskアプリの基本的な機能をカバーし、各ルートが有効な入力に正しく応答し、乗算ルートが無効な入力を適切に処理することを保証します。pytestを使用することで、これらのテストを簡単に実行して、アプリが期待通りに動作していることを確認できます。

ステップ 4 – テストの実行

テストを実行するには、以下のコマンドを実行します。

root@ubuntu:~# pytest

デフォルトでは、pytestのディスカバリープロセスは、現在のフォルダとそのサブフォルダ内に“test_”という名前で始まるや“_test”という名前で終わるファイルを再帰的にスキャンします。これらのファイルに配置されたテストが実行されます。以下のような出力が表示されるはずです。

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

これはすべてのテストが成功して終了したことを示します。

ステップ 5: pytestでフィXTリプトの使用

フィXTリプトはテストにデータやリソースを提供するために使用される関数です。フィXTリプトはテスト環境の設定と解体、データの読み込みや他の設定作业を行うことができます。在pytestでは、フィXTリプトは@pytest.fixtureデコレーターを使用して定義されます。

以下は既存のフィXTリプトを強化する方法です。クライアントのフィXTリプトを設定と破棄のロジックを使用して更新します。

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # テストが行われる場所
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

この設定は、テスト出力に設定と破棄フェーズを示す印刷ステートメントを追加します。必要であれば、実際のリソース管理コードに置き換えることができます。

テストを再実行してみましょう:

root@ubuntu:~# pytest -vs

`-v`フラグは詳細を増やし、`-s`フラグは印刷ステートメントをコンソール出力に表示することができます。

以下の出力を見ることができるはずです:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

Step 6: Adding a Failure Test Case

既存のテストファイルに失敗のテストケースを追加しましょう。`test_app.py`ファイルを修正し、以下の関数を最後に追加して、間違った結果の失敗テストケースを作成してみましょう:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
    # Test with zero
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

    # Test with large numbers (this might fail if not handled properly)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # Intentional failing test: incorrect result
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

`test_multiply_edge_cases`関数を分解し、各部分が何を行うのか説明しましょう:

  1. Test with zero:
    このテストは、multiply関数がゼロの倍数を正しく処理しているかどうかを確認します。ゼロに何か数を乘するときに結果が0になることを期待します。これは、ゼロの倍数についてのテストが重要であることを確認する必要があります。一部の実装は、ゼロの倍数についての問題があるかもしれません。

  2. 大きな数のテスト:
    このテストは、乗算関数が大きな数を処理することができて、オーバーフローまたは精度問題がないかを確認します。ここでは、2つの100万の値を積むことで、1兆の結果を期待しています。このテストは、関数の能力の上限を確認するために重要です。ただし、サーバーの実装が大きな数を適切に処理していない場合は、これは大数ライブラリや異なるデータ型が必要かもしれないことを示すかもしれません。

  3. 意図された失敗テスト:
    このテストは、意図的に失敗させられたものです。2 * 3が7に等しいかを確認しますが、これは間違っています。このテストは、テスト出力にどのように失敗したテストが表示されるかを示すことで、失敗したテストを认识し、デバッグする方法を理解することができるから、重要なスキルを示しています。これは、テスト駆動の開発やデバッグプロセスにおいて、失敗したテストを認識し、デバッグするスキルが不可欠であることを示しています。

これらの境界条件と意図的な失敗を含めることで、あなたは乘算ルートの基本機能だけでなく、極限条件下での行動とエラー報告能力をテストすることができます。このテスト手法は、アプリケーションの強固さと信頼性を保証するために役立ちます。

テストを再実行してみましょう:

root@ubuntu:~# pytest -vs

以下のような出力を見ることが望まれます:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # ゼロの場合のテスト response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # 大きな数字の場合のテスト (適切に処理されない場合は失敗するかもしれません) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # 意図された失敗なテスト: 不正な結果 response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

上の失敗メッセージは、tests/test_app.pyファイルのtest_multiply_edge_casesテストを失敗させました。具体的には、このテスト関数の最後のアサーションが失敗原因です。

この意図された失敗は、テストの失敗がどのように報告されるかを示すために有用であり、失敗メッセージに提供される情報を示します。これにより、失敗が発生した行の正確な位置、期待される値と実際の値、そして两者の差を表示します。

実際のケースでは、テストを通るためにコードを修正するか、期待される結果が間違っている場合はテストを調整する必要があります。しかし、この場合、失敗は教育のために意図的にされています。

結論

このチュートリアルでは、pytestを使用したFlaskアプリケーションの单元テストの設定、pytest固定装置の統合、およびテスト失敗の状態を示しました。これらの手順に従って、Flaskアプリケーションの信頼性と maintainabilityを確保し、バグを最小限に抑え、コードの品質を向上させることができます。

详しくは、FlaskPytestの公式文書に従って学ぶことができます。

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask