pytest用例管理

晴天也要打伞發表於2024-10-23

pytest用例管理

1.安裝

pip install pytest

2.編寫

2.1 編寫規則

  1. 用例檔案:所有檔名以test_開頭
  2. 用例類:測試檔案中每個Test開頭的類就是一個測試用例類
  3. 測試用例:測試類中每一個以test開頭的方法就是一個測試用例

2.2 函式形式編寫測試用例

def test_demo():
    assert 100 == 100

使用pytest可以執行所有測試用例

2.3 以類的形式編寫用例

class Test_demo:
    def test_demo1():
        assert 100 == 100
    
    def test_demo2():
        assert 100 == 200

3.執行測試

3.1 執行方式

  1. 命令列在檔案入口執行pytest
  2. 程式碼中透過 pytest.main()執行

3.2 執行引數

  1. -v 顯示測試的詳細引數資訊

    命令列:pytest -v

    程式碼:pytest.main("-v")

  2. -s 顯示測試執行的詳細資訊

  3. -vs 展示全部資訊

3.3 引數傳遞

  1. -k 匹配用例名詞(當前路徑下的 檔名、類名、方法名,備註可以結合or,not 等關鍵字 ,不區分大小寫)

    pytest.main(['-vs', '-k', 'up', './test_2.py'])

  2. -x 遇到錯誤停止

    `pytest.main(['-vs','-x', './test_stop.py'])

  3. --maxfail=num 失敗數量達到num個時停止

    pytest.main(['-vs', '--maxfail=2', './test_stop.py'])

  4. -n=num 開啟num個執行緒去執行用例 (需要安裝 pytest-xdist)

    pytest.main(['-vs','./test_thread.py','-n=2'])

  5. --reruns=num 設定失敗重跑次數 如果存在用例失敗,用例失敗後重跑的次數num

    pytest.main(['-vs', './test_fail.py', '--reruns=2'])

3.4 指定執行的測試目錄和檔案

::用於指定測試類和測試用例

pytest testcase/test_demo::Test_demo::test_demo

4.前後置方法和fixture機制

4.1 函式用例的前後置方法

def setup_function(function):
    print("前置方法")

def teardown_function(function):
    print("後置方法")

def test_0():
    print("執行")

4.2 測試類中的前後置方法

class Test_demo:
    def test_0():
        print("執行0")
    
    def test_1():
        print("執行1")
   
    @classmethod
    def setup_class(cls):
        print("前置方法")
     
    @classmethod
    def teadown_class(cls):
        print("後置方法")

4.3 測試用例前後置方法(先執行測試類方法,再執行函式測試用例)

class Test_demo:
    def test_0():
        print("執行0")
    
    def test_1():
        print("執行1")
   
    @classmethod
    def setup_class(cls):
        print("執行測試類前置")
    
    @clasmethod
    def teadown_class(cls):
        print("執行測試類後置")
    
     def setup_method(function):
        print("測試用例前置")

    def teardown_method(function):
        print("測試用例後置")

4.4 模組級別前後置方法

def setup_module(module):
    print()

def teardewn_module(module):
    print()

4.5 fixture機制

4.5.1 測試夾具級別

夾具定義可以透過引數scope指定夾具的級別,如果不指定夾具級別,scope 預設值為function(用例級別)

function 每個函式或方法都會呼叫
class 每個類呼叫一次,一個類中存在多個方法
module 每個.py檔案呼叫一次,一個檔案中存在多個class和多個function
session 多個檔案呼叫一次,可以跨.py檔案使用,每個檔案就是一個module

4.5.2 呼叫fixture

  1. 函式或類裡面方法直接傳fixture的函式名稱

    import pytest
    
    @pytest.fixture(scope='function')
    def test1():
        print("前置夾具")
    
    def test_01(test1):
        print("測試函式")
    
    class Test_02:
        def test_02(self, test1):
            print("類方法")
    
  2. 使用裝飾器@pytest.mark.usefixtures()

    import pytest
    
    @pytest.fixture(scope='function')
    def test1():
        print("前置夾具")
    
    @pytest.mark.usefixtures('test1')
    def test_01():
        print("測試函式")
    
    @pytest.mark.usefixtures('test1')
    class Test_01:
        def test_02(self):
            print("類方法")
        
    
  3. 疊加usefixtures

    import pytest
    
    @pytest.fixture(scope='function')
    def test1():
        print("前置夾具1")
    
    @pytest.fixture(scope='function')
    def test2():
        print("前置夾具2")
    @pytest.mark.usefixtures("test1")
    @pytest.mark.usefixtures("test2")
    # 按照順序執行夾具
    class Test_01:
        def test_01(self):
            print("類方法")
    
  4. fixture自動執行 autouse=True

    適用於多用例情況,直接執行

    import pytest
    
    @pytest.fixture(scope='function', autore=True)
    def test1():
        print("前置夾具1")
    
    @pytest.fixture(scope='function', autore=True)
    def test2():
        print("前置夾具2")
    
    class TestCase:
        def test_01(self):
            print("類方法")
    

4.5.3 yield和終結函式

  1. yield

    @pytest.fixture(scope="function")
    def test1():
        print("開始執行")
        yield 1  # 不需要返回值時直接 yield
        print("執行結束")
        
    # 起到分割作用,yield之前的程式碼為setup,後面為teardown
    # 可以實現引數傳遞
    
    def test2(test1):
        print(test1) # 1
    

    存在多個fixture存在yield時,先按照線性順序執行前置,執行過後,反向執行yield後的邏輯

5. conftest.py

用於管理fixture,工程根目錄下設定的conftest.py檔案起到全域性作用,不同子目錄下只在該層級以及一下目錄生效

5.1 注意事項

  1. pytest會預設讀取conftest.py裡面的所有fixture
  2. conftest.py檔名是固定的,不能移動
  3. conftest.py只對同一個package下的所有測試用例生效
  4. 不同目錄可以有自己的conftest.py,一個專案可以有多個conftest.py
  5. 測試用例檔案不用手動import,pytest會自動查詢

5.2 舉例

conftest.py

import pytest

@pytest.fixture(score="session")
def login():
    print("登入")
    name = "test"
    token = ""
    yield name, token # 起到分割作用,yield之前的程式碼為setup,後面為teardown
    print("退出登入")

@pytest.fixture(autose=True)
def get_info(logins):
    name, token = logins
    print(f"列印token: {token}")

get_info(next(login()))

test1.py

def test_get_info(login):
    name, token = login
    print(f"使用者名稱:{}, token{}")

run.py

import pytest

if __name__ == '__main__':
    pytest.main(["-s", "目錄"])

6. 引數化和資料驅動

6.1 fixture傳參

@pytest.fixture(scope="function", params=[1, 2, 4])
def need_data(request):
    yield request.param

def test_001(need_data):
    print(need_data)# 依次輸出1, 2, 4,經歷三次測試

6.2 mark.parametrize資料驅動

class Test_add:
    @pytest.mark.parametrize("arg", [1, 2, 3])
    def test_01(self, arg):
        print(arg)   # 三次測試

read_yaml.py

class Read_Yaml:
    def __init__(self, filename):
        self.filename = filename

    def read_yam(self):
        with open(self.filename, "r", encoding="utf-8") as f:
            context = yaml.load(f, Loader=yaml.Loader)
        return context

requests_paclage.py

class PacKag:
    def __init__(self, api_data):
        self.api_data = api_data

    def packaging(self):
        if self.api_data["method"] == "get":
            res = requests.request(method=self.api_data["method"],                                                    url=self.api_data["url"],
                                   params=self.api_data["params"],                                                    headers=self.api_data["headers"])
        else:
            data = json.dumps(self.api_data["body"])
            res = requests.request(method=self.api_data["method"],                             url=self.api_data["url"],
            data=data, headers=self.api_data["headers"])
        return res.json()

testcase.py

class Test_001:
    test_data = Read_Yaml(setting.yaml_data).read_yam()
    # 讀取列表巢狀字典,依次取出字典,存在幾個字典進行幾次用例
    @allure.title("使用者登入")
    @pytest.mark.parametrize("arg", test_data)
    def test_case001(self, arg):
        try:
            res = PacKag(arg).packaging()
            assert res["errorMessage"] == arg["msg"]
            logger.info("測試透過,結果為{}".format(res["errorMessage"]))
        except Exception as e:
            logger.error(e)

6.3 fixture+mark.parametrize

在fixture中實現前後置方法,使用mark進行資料驅動

@pytest.fixture(scope='function')
def add_data(request):
    print(request.param) # 根據傳進來的引數依次執行前後置方法
    yield request.param["name"]
class Test_001:
    test_data = Read_Yaml(setting.authentication_yaml).read_yam()

    @allure.title("使用者登入")
    @pytest.mark.parametrize("add_data", test_data, indirect=True)
    # indirect=True,可以指定資料傳入指定的前後置函式
    def test_case001(self, add_data):
        try:
            res = PacKag(add_data).packaging()
            assert res["errorMessage"] == arg["msg"]
            logger.info("測試透過,結果為{}".format(res["errorMessage"]))
        except Exception as e:
            logger.error(e)

6.4 多個傳入變數,列表巢狀元組

class Test_001:

    @allure.title("使用者登入")
    @pytest.mark.parametrize("tt, ll", [(1, 2), (3, 4)])
    # indirect=True,可以指定資料傳入指定的前後置函式
    def test_case001(self, tt, ll):
        try:
            print(tt, ll)
        except Exception as e:
            logger.error(e)
    # 第一次輸出1,2   第二次輸出3,4

6.5 引數組合測試

username = ["lilei", "hanmeimei"]
password = ["123456", "888888"]
@pytest.mark.parametrize("password", password)
@pytest.mark.parametrize("username", username)
def test_login(username, password):
    headers = {"Content-Type": "application/json;charset=utf8"}
    url = "http://127.0.0.1:5000/login"
    _data = {
        "username": username,
        "password": password
    }
    res = requests.post(url=url, headers=headers, json=_data).text
    res = json.loads(res)
    assert res['code'] == 1000
# 同時存在多個mark時,會採用列舉兩兩組合,上文存在四次測試用例

6.5 引數說明(ids)

@pytest.mark.parametrize("args, tt", [(1, 2), (3, 4)], ids=["a:{}-b:{}".format(a, b) for a, b in [(1, 2), (3, 4)]])

# 輸出testcase.py::Test_002::test_case002[a:1-b:2].......

7.外掛

7.1測試執行順序:pytest-ordering

預設執行順序從上而下,使用pytest-ordering實現自定義執行順序

pip install pytest-ordering

import pytest

class TestOrder:

    def test_e(self):
        print("test_e")

    def test_4(self):
        print("test_4")


def test_b():
    print("test_a")

@pytest.mark.run(order=2)
def test_a():
    print("test_a")

@pytest.mark.run(order=1)
def test_2():
    print("test_2")

def test_1():
    print("test_1")


if __name__ == '__main__':
    pytest.main()

7.2 失敗後重新執行:pytest-rerunfailures

測試失敗後要重新執行n次,間隔n秒後執行

執行命令 pytest -v --reruns 5 --reruns-delay 1,每次等待1秒,重新執行5次

7.3 一個用例多個斷言,前面失敗後依然可以執行

pytest-assume

pytest.assume(1 == 1)
pytest.assume(1 == 2)
# 使用這種斷言不會執行try except,詳情需要實時日誌

7.4 多程序並行和分散式執行

pytest-xdist

pytest -v test.py -n 5,五個程序執行

8.標記用例執行

只執行標記的部分用例,指定執行某一類或某個場景的測試用例

8.1 pytest.ini

[pytest]

markers =
    smoke: marks test as smoke
    login
    order: 下單場景
@pytest.mark.smoke
def test_01():
    print("執行test_01")

def test_02():
    print("執行test_02")

8.2 conftest.py

定義鉤子函式

def pytest_configure(config):
    marker_list = [
        "smoke: ok",
        "login",
        "order: 場景"
    ]
    for marker in marker_list:
        config.addinivalue_line("markers", marker)
import pytest

# 標記測試函式
@pytest.mark.smoke
def test_01():
    print("執行test_01")

def test_02():
    print("執行test_02")

    
# 標記測試類
@pytest.mark.order
class TestOrder:
    
    def test_order(self):
        print("下單")

    def test_pay(self):
        print("支付")

        
# 多個標籤 
@pytest.mark.smoke
@pytest.mark.login
def test_login():
    print("登入")

8.3 執行

pytest -m smoke 執行單個用例

pytest -m smoke and login執行多個用例

pytest -m smoke or login 執行或

pytest.main(['-m', 'smoke and login']) 不能在當前模組執行,需要新建run.py,分離執行才會生效

8.4 標記跳過

import pytest

@pytest.mark.skip(reason="不需要執行test_01")
def test_01():
    print("執行test_01")

@pytest.mark.skip(2>1, reason="如果2大於1則跳過不執行")
def test_02():
    print("執行test_02")
    
    
if __name__ == '__main__':
    pytest.main(['-s'])

8.5 標記執行失敗

部分場景:對未實現的功能或者尚未修復的錯誤進行測試,使用@pytest.mark.xfail可以將測試用例標記為失敗

pytest.mark.xfail(condition=None, reason=None, raises=None, run=True, strict=False)
condition: 預期失敗的條件
reason: 失敗原因,會在控制檯展示
run: 預設值為True,當run為false時,pytest不會執行該用例,直接將結果標記為xfail
strict: 當 strict=False 時,如果用例執行失敗,則結果標記為xfail,表示符合預期的失敗;如果用例執行成功,結果標記為XPASS,表示不符合預期的成功;

當strict=True時,如果用例執行成功,結果將標記為failed。

import pytest

# run、strict都為預設,因為用例執行是失敗的,所以該用例執行結果會被標記為xfail
@pytest.mark.xfail(reason="bug待修復")
def test_01():
    print("執行test_01")
    a = "hello"
    b = "hi"
    assert a == b

# run、strict都為預設,因為用例執行是透過的,所以該用例執行結果會被標記為xpass
@pytest.mark.xfail(condition=lambda: True, reason="bug待修復")
def test_02():
    print("執行test_02")
    a = "hello"
    b = "hi"
    assert a != b

# run=False,該用例不執行,直接將結果標記為xfail
@pytest.mark.xfail(reason="功能尚未開發完成", run=False)
def test_03():
    print("執行test_03")
    a = "hello"
    b = "hi"
    assert a == b

# strict=True,因為用例執行是透過的,所以結果會被標記為failed
@pytest.mark.xfail(reason="功能尚未開發完成", strict=True)
def test_04():
    print("執行test_04")
    a = "hello"
    b = "he"
    assert b in a
    
if __name__ == '__main__':
    pytest.main(['-s'])

9.實時日誌(方便監控一個用例多個斷言)

9.1 pytest.ini

[pytest]
log_cli = 1
log_cli_leavl = INFO
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)

9.2 直接使用pytest- o

pytest test.py -o log_cli=true -o log_cli_level=INFO

格式: -vv -o log_cli=true -o log_cli_level=INFO --log-date-format="%Y-%m-%d %H:%M:%S" --log-format="%(filename)s:%(lineno)s %(asctime)s %(levelname)s %(message)s"

10.測試報告

10.1 pytest-html

pip install pytest-html

  1. pytest.main執行

    pytest.mian(["--html=測試報告.html"])在用例檔案中執行該命令不能生成測試報告,需要單獨開一個run.py檔案執行

  2. 命令列執行:

    pytest test.py --html=測試報告.html

    pytest test.py --html=E:測試報告.html

10.2 allue

10.2.1 安裝

  1. 安裝pytest-allure: pip install pytest-allure
  2. 安裝allure
  3. 配置環境變數:path: D:\allure\allure-2.15.0\bin

10.2.2 特性

@allure.epic() 描述被測軟體系統
@allure.feature() 描述被測軟體的某個功能模組
@allure.story() 描述功能模組下的功能點和測試場景
@allure.title() 定義測試的標題
@allure.description() 測試用例的說明描述
@allure.severity() 標記測試用例級別,由高到低:blocker、critical、normal、mirnor、trivial
@allure.step() 標記通用函式使之成為測試步驟,測試方法中呼叫此函式的地方會像報告中輸出步驟描述
allure.attach() 插入附件 allure.attach(body, name=None, attachment_type=None, extension=None),內容,名稱,型別
allure.attach.file() 插入附件 allure.attach.file(source, name=None, attachment_type=None, extension=None) 內容,名稱,型別
with allure.step() 標記測試步驟
allure.issue("url", name='') 跳轉連結
allure.testcase() 跳轉連結
 @allure.story("使用者退出登入")
 @allure.title("退出登入")
 def test_logout(self):
      '''這條測試用例僅僅只是為了舉例說明allure.attach.file的使用'''
        print("退出登入,並截圖")
        # 截圖路徑
        testcase_path = (os.path.join(os.path.dirname(os.path.abspath(__file__))), "/screenshot/logout.jpg")
        allure.attach.file(
            source=testcase_path,
            name="退出登入後截圖",
            attachment_type=allure.attachment_type.JPG # 圖片型別
        )
        assert True

with allure.step()

 with allure.step("請求登入介面"):
        allure.attach(
                body="使用者名稱-{},密碼-{}".format(username, password),
                name="登入引數",
                attachment_type=allure.attachment_type.TEXT
            )
         res = requests.post(url=url, headers=headers, json=_data).text
         res = json.loads(res) 
           
  # 第二步,獲取返回引數進行斷言
 with allure.step("斷言"):
       assert res['code'] == 1000
# with 方式一般用於截圖或者檔案
    
    
# with allure.step("步驟詳情"):
      # 具體用例
# 或者裝飾器,這種可以展示步驟傳參
@allure.step("step:新增購物車")
def add_shopping_cart(goods_id="10086"):
    '''新增購物車'''
    print("新增購物車")

10.2.4 生成測試報告

  1. pytest.main()

    pytest.main(['test.py', '-s', '-q', '--alluredir', './result'])
    # 執行test.py測試用例,將測試結果以json的格式儲存在當前目錄的result資料夾中
    os.system('allure generate ./result -o ./report --clean')
    執行json資料,並生成測試報告,儲存在當前目錄的report資料夾中, --clean刪除之前生成的測試報告
    

    run.py

    if __name__ == '__main__':
        pytest.main(['testcase/test_case.py', '-s', '-q', '--alluredir', './result'])
        os.system('allure generate ./result -o ./report --clean')
    
  2. 命令列

    pytest testcase/test_case.py --alluredir ./result

    allure generate ./result -o ./report --clean 然後 allure open ./report 或者 allure serve ./report

10.2.5 定製部分模板

  1. envionment,展示此次測試執行的環境資訊

    建立envionment.properties,放在--alluredor指定的資料夾

    system=win
    python=3.7.7
    version=1.0.1
    host=127.0.0.1
    test=孔振國
    
  2. categories,用於定製缺陷分類,建立categories.json配置,跟上一個相同目錄

    [
      {
        "name": "Passed tests", 
        "matchedStatuses": ["passed"]
      },
      {
        "name": "Ignored tests",
        "matchedStatuses": ["skipped"]
      },
      {
        "name": "Infrastructure problems",
        "matchedStatuses": ["broken", "failed"],
        "messageRegex": ".*bye-bye.*"
      },
      {
        "name": "Outdated tests",
        "matchedStatuses": ["broken"],
        "traceRegex": ".*FileNotFoundException.*"
      }
    ]
    

11.CI/CD jenkins

11.1 docker 安裝jenkins並更新版本

docker pull jenkens/jenkins

docker run -it --name jenkins -p 8080:8080 -p 5000:5000 jenkins/jenkins

docker cp jenkins.war jenkins/jenkins: var/share/jenkins

docker restart jenkins/jenkins

11.2 安裝Allure外掛並進行全域性工具配置

本地安裝直接取消自動安裝並寫入allure安裝目錄

11.3 新建windows節點,並啟動

​ 新建本地執行環境

curl.exe -sO http://47.92.76.123:8080/jnlpJars/agent.jar

java -jar agent.jar -jnlpUrl http://47.92.76.123:8080/computer/windows/jenkins-agent.jnlp -secret 724f3a87c3cf0c7278c35d365a28c9b32345c1fccc5859dc2a3ac7a3546714a4 -workDir "E:\Jenkins"

11.4 新建任務

  1. 限制專案的執行節點
  2. 使用自定義工作空間
  3. 構建前命令 pytest testcase.py -vs --alluredir ./allure
  4. 構建後新增Allure Report 指定json資料目錄,並指定生成報告目錄

12.鉤子函式

12.1 切換測試環境

  1. Hook方法

    conftest.py

    註冊一個自定義的命令列引數

    def pytest_addoption(parser):
        parser.addoption("--env", action="store", default="url", help="將--env新增到pytest配置中")
        # action="store",允許使用者儲存任何型別的值
        # default 在--env不填值時,預設值
        # help 說明
    
    @pytest.fixture(scope="session")
    def get_env(request):
        """從配置物件中讀取自定義引數的值"""
        return request.config.getoption("--env")
    
    # 可以不用寫
    @pytest.fixture(autouse=True)
    def set_env(get_env):
        """將自定義引數的值寫入全域性配置檔案"""
        with open(ENV_TXT_FILE, 'w', encoding='utf-8') as f:
            f.write(get_env)
    

    test_demo.py

    def test_001(get_env):
        print(get_env)
    # 傳入主機名,測試用例中可以透過使用不同的主機切換測試環境
    
  2. 使用pytest-base-url外掛

    1. 安裝pytest-base-url pip install pytest-base-url -i https://pypi.douban.com/simple

    2. 將base-url引數傳入fixture函式中

      @pytest.fixture
      def driver_setup(base_url):
          try:
              URL = base_url
              start_chrome(URL, options=browser_options(), headless=False)
              driver = get_driver()
          except Exception as e:
              log.error(e)
          else:
              yield driver
      # 如果發生異常,對異常操作,如果正常,執行eles裡面的邏輯
      #或者直接使用
      def test_001(base_url):
          print base_url
      
    3. pytest命令列傳參

      pytest --base-url https://www.cnblogs.com/

相關文章