Pytest系列(30)- 使用 pytest-xdist 分散式外掛,如何保證 scope=session 的 fixture 在多程式執行情況下仍然能只執行一次

小菠蘿測試筆記發表於2021-04-27

如果你還想從頭學起Pytest,可以看看這個系列的文章哦!

https://www.cnblogs.com/poloyy/category/1690628.html

 

背景

  • 使用 pytest-xdist 分散式外掛可以加快執行,充分利用機器多核 CPU 的優勢
  • 將常用功能放到 fixture,可以提高複用性和維護性
  • 做介面自動化測試的時候,通常我們會將登入介面放到 fixture 裡面,並且 scope 會設定為 session,讓他全域性只執行一次
  • 但是當使用 pytest-xdist 的時候,scope=session 的 fixture 無法保證只執行一次,官方也通報了這一問題

 

官方描述

  • pytest-xdist 的設計使每個工作程式將執行自己的測試集合並執行所有測試子集,這意味著在不同的測試過程中,要求高階範圍的 fixture(如:session)將會被多次執行,這超出了預期,在某些情況下可能是不希望的
  • 儘管 pytest-xdist 沒有內建支援來確保  scope=session 的fixture 僅執行一次,但是可以通過使用鎖定檔案進行程式間通訊來實現

 

前置知識

pytest-xdist 分散式外掛使用詳細教程可看

https://www.cnblogs.com/poloyy/p/12694861.html

 

分散式外掛原理可看

https://www.cnblogs.com/poloyy/p/12703290.html

 

fixture 的使用詳細教程

https://www.cnblogs.com/poloyy/p/12642602.html

官方文件

https://pypi.org/project/pytest-xdist/

 

官方解決辦法(直接套用就行)

import json

import pytest
from filelock import FileLock


@pytest.fixture(scope="session")
def session_data(tmp_path_factory, worker_id):
    if worker_id == "master":
        # not executing in with multiple workers, just produce the data and let
        # pytest's fixture caching do its job
        return produce_expensive_data()

    # get the temp directory shared by all workers
    root_tmp_dir = tmp_path_factory.getbasetemp().parent

    fn = root_tmp_dir / "data.json"
    with FileLock(str(fn) + ".lock"):
        if fn.is_file():
            data = json.loads(fn.read_text())
        else:
            data = produce_expensive_data()
            fn.write_text(json.dumps(data))
    return data
  • 若某個 scope = session 的 fixture 需要確保只執行一次的話,可以用上面的方法,直接套用,然後改需要改的部分即可(這個後面詳細講解)
  • 官方原話:這項技術可能並非在每種情況下都適用,但對於許多情況下,它應該是一個起點,在這種情況下,對於 scope = session 的fixture 只執行一次很重要

 

後續栗子的程式碼

專案結構

xdist+fixture(資料夾)
│  tmp(存放 allure 資料資料夾)
│  conftest.py
│  test_1.py
│  test_2.py
│  test_3.py
│ __init__.py │ 

 

test_1.py 程式碼

import os


def test_1(test):
    print("os 環境變數",os.environ['token'])
    print("test1 測試用例", test)

 

test_2.py 程式碼

import os


def test_2(test):
    print("os 環境變數",os.environ['token'])
    print("test2 測試用例", test)

 

test_3.py 程式碼

import os


def test_3(test):
    print("os 環境變數",os.environ['token'])
    print("test3 測試用例", test)

 

未解決情況下的栗子

conftest.py 程式碼

import os
import pytest
from random import random


@pytest.fixture(scope="session")
def test():
    token = str(random())
    print("fixture:請求登入介面,獲取token", token)
    os.environ['token'] = token
    return token

 

執行命令

pytest -n 3 --alluredir=tmp

 

執行結果

scope=session 的 fixture 很明顯執行了三次,三個程式下的三個測試用例得到的資料不一樣,明顯不會是我們想要的結果

 

使用官方解決方法的栗子rt 

 

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
__title__  = 
__Time__   = 2021/4/27 11:28
__Author__ = 小菠蘿測試筆記
__Blog__   = https://www.cnblogs.com/poloyy/
"""
import json
import os
import pytest
from random import random
from filelock import FileLock

@pytest.fixture(scope="session")
def test(tmp_path_factory, worker_id):
    # 如果是單機執行 則執行這裡的程式碼塊【不可刪除、修改】
    if worker_id == "master":
        """
        【自定義程式碼塊】
        這裡就寫你要本身應該要做的操作,比如:登入請求、新增資料、清空資料庫歷史資料等等
        """
        token = str(random())
        print("fixture:請求登入介面,獲取token", token)
        os.environ['token'] = token

        # 如果測試用例有需要,可以返回對應的資料,比如 token
        return token

    # 如果是分散式執行
    # 獲取所有子節點共享的臨時目錄,無需修改【不可刪除、修改】
    root_tmp_dir = tmp_path_factory.getbasetemp().parent
    # 【不可刪除、修改】
    fn = root_tmp_dir / "data.json"
    # 【不可刪除、修改】
    with FileLock(str(fn) + ".lock"):
        # 【不可刪除、修改】
        if fn.is_file():
            # 快取檔案中讀取資料,像登入操作的話就是 token 【不可刪除、修改】
            token = json.loads(fn.read_text())
            print(f"讀取快取檔案,token 是{token} ")
        else:
            """
            【自定義程式碼塊】
            跟上面 if 的程式碼塊一樣就行
            """
            token = str(random())
            print("fixture:請求登入介面,獲取token", token)
            # 【不可刪除、修改】
            fn.write_text(json.dumps(token))
            print(f"首次執行,token 是{token} ")

        # 最好將後續需要保留的資料存在某個地方,比如這裡是 os 的環境變數
        os.environ['token'] = token
    return token

 

執行命令

pytest -n 3 --alluredir=tmp

 

執行結果

可以看到 fixture 只執行了一次,不同程式下的測試用例共享一個資料 token

 

重點

  • 讀取快取檔案並不是每個測試用例都會讀,它是按照程式來讀取的
  • 比如 -n 3 指定三個程式執行,那麼有一個程式會執行一次 fixture(隨機),另外兩個程式會各讀一次快取
  • 假設每個程式有很多個用例,那也只是讀一次快取檔案,而不會讀多次快取檔案
  • 所以最好要將從快取檔案讀出來的資料儲存在特定的地方,比如上面程式碼的 os.environ 可以將資料儲存在環境變數中

 

兩個程式跑三個測試用例檔案

還是上面栗子的程式碼

執行命令

pytest -n 2 --alluredir=tmp

 

執行結果

可以看到 test_3 的測試用例就沒有讀快取檔案了,每個程式只會讀一次快取檔案,記住哦!

 

相關文章