如何在 Flask 中進行單元測試

引言

測試是軟件開發過程中不可或缺的一環,它確保程式碼如预期般行為且無缺陷。在Python中,pytest是一個流行的測試框架,它提供比標準unit test模組更多的優點,後者是一個內置的Python測試框架,也是標準庫的一部分。pytest具有更簡單的語法、更好的輸出、強大的固定器以及豐富的插件生態系統。本教程將引导您 setting up a Flask application, integrating pytest fixtures, and writing unit tests using pytest

前提

在開始之前,您需要以下內容:

  • 運行Ubuntu的服务器和一个具有sudo權限的非root用戶以及活躍的防火牆。要了解如何設置,請從這個列表中選擇您的發行版,並遵循我們的初始服務器設定指南。請確保使用受支持的Ubuntu版本

  • 熟悉 Linux 命令行。您可以查看這份Linux 命令行简介指南。

  • 對 Python 程式設計和 Python 中的 pytest 測試框架有基本理解。您可以參考我們的教程PyTest Python 測試框架,以了解更多關於 pytest 的資訊。

  • 您的 Ubuntu 系統上必須安裝 Python 3.7 或更高版本。如果您想學習如何在 Ubuntu 上運行 Python 脚本,您可以參考我們關於 如何在 Ubuntu 上運行 Python 脚本 的教程。

為什麼 pytest 是比 unittest 更好的選擇

pytest 比內置的 unittest 框架提供了一些優點:

  • Pytest 允許您用較少的样板代碼來撰寫測試,使用簡單的 assert 声明而不是 unittest 所需要的較為冗長的方法。

  • 它提供更多詳細且易讀的輸出,讓你能更容易地確定測試失敗的位置和原因。

  • Pytest fixture 允許比 unittestsetUptearDown 方法更具彈性和重用性的測試設定。

  • 它讓同一測試函數用於多組輸入值變得簡單,在 unittest 中並不是那麼直觀。

  • Pytest 擁有丰富的插件庫,從代碼覆蓋工具到並行測試執行,都可 extending its functionality.

  • 它自動發現符合其命名惯例的測試文件和函數,節省管理和維護測試套件的時間和努力。

考慮到這些好处,`pytest` 通常是她 Modern Python 測試的首選。讓我們建立一個 Flask 應用程序並使用 `pytest` 寫单元測試。

步驟 1 – 設定環境

Ubuntu 24.04 默認搭載 Python 3。開啟終端並運行以下命令以核對 Python 3 的安裝:

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

如果您的電腦上已經安裝了 Python 3,上述命令將返回 Python 3 當前安裝的版本。 如果尚未安裝,您可以運行以下命令來獲得 Python 3 的安裝:

root@ubuntu:~# sudo apt install python3

接下來,您需要在系統上安裝 `pip` 包安裝器:

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

使用 pip 安装 Flask:

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)

這個應用程序有三個路由:

  • /: 返回一個簡單的 “你好,Flask!” 訊息。
  • /about: 返回一個簡單的 “這是關於頁面” 訊息。
  • /multiply/<int:x>/<int:y>: 乘以兩個整數並返回結果。

要以執行以下命令來運行應用程序:

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/
    這會將一個 GET 請求發送到我們 Flask 應用程式的根路徑 (‘/’)。服務器回傳一個包含讯息 “Hello, Flask!” 的 JSON 物件,展示我們首頁路徑的基本功能。

  2. curl http://127.0.0.1:5000/about
    這會將一個 GET 請求發送到 /about 路徑。服務器回傳一個包含讯息 “This is the About page” 的 JSON 物件。這顯示我們的路徑正常運作。

  3. curl http://127.0.0.1:5000/multiply/10/20
    這會將一個 GET 請求發送到 /multiply 路徑,並帶兩個參數:10 和 20。服務器乘以這些數字,並回傳一個包含結果 (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}

步驟3 – 安装 pytest 並撰寫您的第一個測試

既然您已經有了一個基本的Flask應用程序,讓我們來安裝pytest並撰寫一些單元測試。

使用pip來安裝pytest

root@ubuntu:~# pip install pytest

建立一個用於存儲測試文件的tests資料夾:

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__))))

# 從主app文件導入Flask app實例
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)。它檢查狀態碼為200,並驗證JSON響應的內容。

  4. def test_multiply(client):
    這個函數測試帶有有效輸入的乘法路由(/multiply/3/4)。它檢查狀態碼是否為200,並且JSON響應是否包含正確的乘法結果。

  5. def 測試乘法無效輸入(client):
    這個函數用來測試乘法路徑與無效的輸入(multiply/three/four)。它檢查狀態碼是否為 404 (找不到),當路徑無法配對字串輸入到必要的整數參數時,這是一種预期的行為。

  6. def 測試不存在路徑(client):
    這個函數用來測試應用程序當訪問不存在的路徑時的行為。它向 /non-existent 發送 GET 請求,這在我們的 Flask 應用程序中沒有定義。測試確保响应狀態碼是 404 (找不到),這確保了我們的應用程序正確處理對未定義路徑的請求。

這些測試覆蓋了我們 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 中使用固定值

固定值是用於提供數據或資源給測試的函數。它們可用來設定和拆除測試環境,加載數據,或執行其他設定任務。在 pytest 中,固定值是使用 @pytest.fixture 裝飾器來定義的。

以下是如何增強现有固定值的方法。更新客戶固定值以使用設定和解除設定邏輯:

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 =============================================

步驟 6:添加一個失敗測試案例

讓我們將一個失敗的測試案例添加到現有的測試文件中。修改 `test_app.py` 文件,在文件末尾添加以下函數以為錯誤結果添加一個失敗測試案例:

test_app.py
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"

讓我們分解 `test_multiply_edge_cases` 函數並解釋每個部分的作用:

  1. 零的測試:此測試檢查 `multiply` 函數是否正確處理零的乘法。當任何數字乘以零時,我們期望結果為 0。测试零乘法是重要的邊界情況,因為一些實現可能會存在零乘法問題。

  2. 大數字測試:
    此測試用於驗證乘法功能是否能夠處理大數字而不出现过溢或精度問題。我們將兩個一百万的值相乘,期望結果為一兆。此測試至關重要,因為它檢查了功能的上限能力。注意,如果服務器的實現不正確地處理大數字,這可能表明需要大數字庫或不同的數據類型。

  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 測試 fails。特別是,這個測試功能中的最後一個断言引起了失敗。

這個故意的失敗對於展示測試失敗是如何報告的以及失败消息中提供了哪些信息很有用。它顯示失敗發生在哪一行,预期的和實際的值,以及兩者之間的差異。

在實際情況下,您會修正代碼以讓測試通過,或者如果预期的結果不正確,則調整測試。然而,在此案例中,失利是故意的,目的是為了教育。

結論

在這個教學中,我們涵蓋了如何使用pytest為Flask應用程序設定單元測試,整合pytest測試 fixture,並展示了一個測試失利的外观。通過遵循這些步驟,您可以確保您的Flask應用程序是可靠和可維護的,最小化BUG並提高代碼質量。

您可以參考FlaskPytest官方文件來學習更多。

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