Python Note 03

介紹pytest的fixture、conftest功能,透過pytest的一些插件來產生報表。

Python Note 03

基於Pytest框架的自動化測試 (下)

前面介紹了直接使用測試函數、用class包裝測試、setupteardown、參數化測試案例、驗證預期的錯誤和跳過錯誤,這些是pytest基本的測試方法,接下來會透過pytest的fixture、conftest來整合這些功能,還有透過pytest的一些插件來產生報表,讓pytest的功能變得更加強大和靈活。

進階功能

接下來介紹的是 pytest 的進階功能,分別有fixtureconftest和產生報表的方法。

fixture

fixtures是pytest中的一個關鍵概念,他有前面介紹的setupteardown一樣的功能,可以幫助建構測試所需的環境,並在測試函數中重複使用:

import pytest


@pytest.fixture
def setup_teardown_example():
    # 在測試之前執行的代碼
    test_data = "在資料庫中準備測試資料"
    yield test_data
    # 在測試之後執行的代碼
    print("清理測試資料")


def test_using_fixture(setup_teardown_example):
    test_data = setup_teardown_example
    # 使用 fixture
    assert something == expected_result

fixture可使用的參數有下列三種:

  • scope:表示作用域,預設為function
  • name:用來設定fixture的別名,預設為函式名稱
  • autouse:預設為False,若為True,則會自動進行使用 (根據scope作用域而定)

其中fixture的scope作用域跟setupteardown有略微不同,總共有四種,分別為:

  1. function : 預設的選項,在每個測試函數運行之前和之後執行,各測試函數都將獲得一個新的、獨立的fixture實例,不會共享數據。
  2. class : Fixture將在每個測試類 (使用@pytest.mark.usefixtures裝飾器) 中的測試函數之前和之後執行,在多個測試函數之間共享相同的fixture數據非常有用。
  3. module : 在整個測試模塊 (測試文件) 中的測試函數之間執行,對於在模塊級別共享數據非常有用。
  4. session : 整個pytest會話 (所有測試模塊) 中的測試函數之前和之後執行,允許在多個模塊之間共享持久性數據

比較特別的是fixture在class上的應用,以下是使用@pytest.mark.usefixtures的範例,fixture會在測試class的所有方法之前運行一次,會在整個class中共享數據,然後在所有測試方法執行完成後進行清理:

import pytest

@pytest.fixture(scope="class")
def class_fixture():
    # 在測試類別中的測試方法之前和之後執行一次的fixture
    print("Class fixture setup")
    yield
    print("Class fixture teardown")

@pytest.mark.usefixtures("class_fixture")
class TestClassWithFixture:

    def test_method1(self):
        print("Test method 1")

    def test_method2(self):
        print("Test method 2")

以下展示了fixture在各個層級使用的範例,在這個範例中展示了fixture在session創建了與資料庫的連結;在module層級上的指定了測試用的collection並插入數據用於測試;在class層級上新增了測試資料讓後續的class級別的測試方法可以進行測試:

import pytest
from pymongo import MongoClient


# 定義一個具有session級別MongoDB client的fixture
@pytest.fixture(scope="session")
def mongodb_connection():
    # 連接到MongoDB的Client
    client = MongoClient('localhost', 27017)
    # 使用測試用的資料庫
    db = client['mytestdb']
    yield db
    # 清理測試用資料庫
    client.drop_database('mytestdb')


# 定義一個具有module級別的資料庫連接fixture,用於插入Document
@pytest.fixture(scope="module")
def setup_mongodb_data(mongodb_connection):
    # 使用測試集合名稱寫入數據
    collection = mongodb_connection['mycollection']
    data = [
        {"_id": 1, "name": "user1"},
        {"_id": 2, "name": "user2"},
    ]
    collection.insert_many(data)
    yield
    collection.delete_many({})


# 定義一個class級別的fixture
@pytest.fixture(scope="class")
def class_fixture(mongodb_connection, setup_mongodb_data):
    # 在測試的class中所有測試方法之前和之後運行一次的fixture設置
    collection = mongodb_connection['mycollection']
    data = {"_id": 3, "name": "user3"}
    collection.insert_one(data)
    yield
    collection.delete_one({"_id": 3})


# 在測試的class中使用前面設定的fixture進行測試
@pytest.mark.usefixtures("class_fixture")
class TestMongoDBClass:

    def test_fetch_user_data(self, mongodb_connection):
        collection = mongodb_connection['mycollection']
        user = collection.find_one({"_id": 1})
        assert user["name"] == 'user1'
  
    def test_another_test(self, mongodb_connection):
        collection = mongodb_connection['mycollection']
        user = collection.find_one({"_id": 2})
        assert user["name"] == 'user2'
  
    def test_class_fixture(self, mongodb_connection):
        collection = mongodb_connection['mycollection']
        user = collection.find_one({"_id": 3})
        assert user["name"] == 'user3'

conftest

conftest.py是pytest中一個特殊的模塊,它允許在不同的測試文件之間共享 fixture、自定義 hook 函數和配置選項,簡而言之就是一個可以存放經常被使用到 fixture的地方。

pytest在一開始執行時,就會先去抓是否有conftest.py的存在,而存放在conftest.py當中的fixture不需要透過 import就可以直接進行使用,如果有某些fixture會被許多個test module使用的話,就可以試著將該fixture放到conftest.py當中。

conftest.py的名稱和位置是固定的,必須放置在測試項目的根目錄或子目錄下,pytest會自動識別並加載它,conftest.py所存在的當前目錄以及其所有子目路中的test case都可以使用,若於不同目錄則需要另外寫一個conftest.py

使用conftest.py的的資料夾結構可以如下:

├─src
│  ├─pkg1
│  │  ├─__init__.py
│  │  └─module_a.py
│  └─pkg2
│     ├─__init__.py
│     ├─module_a.py
│     └─module_b.py
├─tests
│  ├─test_pkg1
│  │  ├─__init__.py
│  │  └─test_case.py
│  ├─test_pkg2
│  │  ├─__init__.py
│  │  ├─test_case.py
│  │  └─conftest.py
│  ├─__init__.py
│  ├─conftest.py
│  └─test_1.py
└─setup.py

產生報表

前面介紹了許多使用pytest的方法,當使用pytest執行測試時,產生報表也是非常重要的,這是因為測試報表具有以下幾個關鍵用途和優勢:

  1. 可視化測試結果: 測試報表可以將測試結果以易於理解的方式呈現,通常用顏色和標籤來區分通過的測試、失敗的測試和錯誤的測試,使開發人員能夠快速識別和定位問題,並更容易理解測試的運行情況。
  2. 歷史紀錄: 測試報表通常可以保存歷史測試運行的結果,開發團隊可以追蹤測試運行的變化,對於檢測測試覆蓋率的提高或測試性能的改進非常有用。
  3. 集成到CI/CD流程: 在CI/CD流程中,測試報表可以自動生成並保存,這樣團隊可以輕鬆地監視應用程序的測試狀態,並確保每次部屬都是穩定的。如果測試報告顯示問題,則可以停止部屬,以防止潛在的錯誤進入生產環境。
  4. 分析測試結果: 測試報表還可以提供測試運行時間、測試覆蓋率、測試失敗的統計訊息等等,這有助於開發人員進行性能分析和測試覆蓋率分析。

以下是pytest常用產生報表的插件:

  • pytest-json-report: 用於生成JSON格式的測試報告。
    使用方法: pytest --json-report=path/to/report.json
  • pytest-cov: 用於測試覆蓋率報告,可生成HTML供檢視。
    使用方法: pytest --cov=<your_package> --cov-report=html
  • pytest-html: 生成漂亮的HTML測試報告 (4.0.2會有問題,目前使用3.2.0)。
    使用方法: pytest --html=report.html --self-contained-html
  • allure-pytest: Allure是一個開源的測試報告框架,可用於紀錄、展示和分析測試結果,下面單獨介紹如何使用這個套件。

Allure

安裝Allure需要Java JDK支持,需要到這個網站安裝,接下來直接到Allure的Github網址下載最新版本Allure的zip檔,將allure-x.x.x/bin放到系統的PATH中,可以在命令列上輸入allure --version檢查是否安裝成功。

接著要安裝allure在pytest的套件:

poetry add allure-pytest

可以直接在專案內執行以下命令,可以得到測試結果的資料:

pytest --alluredir ./allure/data

可以透過以下命令產生出html檔案,可以將index.html放到瀏覽器中查看報告:

allure generate ./allure/data -o ./allure/report/ --clean

不過用這種方式可能會遇到網頁打開了,但讀取不到數據的問題,這是因為CORS policy error的問題,如果是使用Google Chrome,在啟動時需要加上以下的參數以避免CORS policy error:

--allow-file-access-from-files

或是可以透過以下命令直接建構一個本地Web服務器:

allure serve ./allure/data

Allure還有許多其他的功能,具體可以參考官方文檔,後續有機會也可以專門為Allure寫一篇介紹文。

結論

測試框架在軟件開發過程中扮演著重要的角色,它不僅幫助測試人員高效地執行測試,還有助於開發團隊確保產品質量和穩定性,測試框架是保證軟件品質的不可或缺的工具之一。除錯和測試是軟體開發過程中的正常部分,不要害怕花時間來進行這些工作,每次修復一個錯誤,程式將變得更加穩定和可靠,持續學習和改進測試和除錯技巧也是提高軟體開發效率的關鍵。