Pytest

Pytest介绍

pytest 是 Python 中最流行的单元测试框架之一,相比于自带的 unittest

  • 语法更简洁,使用更灵活

  • 自动发现测试用例

  • 丰富的插件生态

简单的组成结构

import pytest


def test_case01():  # 测试用例1
    print("我是用例主体")


def test_case02():  # 测试用例2
    print("我这里可以写任何测试用例")


if __name__ == "__main__":
    pytest.main(['-s'])  # 执行测试用例 

Pytest的安装和执行

使用pip安装

pip3 install pytest==7.3.1  # 使用pip安装pytest7.3.1版本
pytest --version    # 校验pytest是否成功安装

 命令行中执行

# 执行11pytest路径下所有的测试文件中所有测试用例
 pytest 11pytest 

# 执行test_01pytest执行.py文件中所有的测试用例
 pytest 11pytest/test_01pytest执行.py 

# 执行test_01pytest执行.py里面test_case01这条测试用例
 pytest 11pytest/test_01pytest执行.py::test_case01

参数说明

官方文档参数说明:https://docs.pytest.org/en/stable/how-to/usage.html

-s 开启终端交互

作用是可以让打印的内容输出显示在终端,或者可以在终端中与用例中的输入操作进行交互

 pytest -s 11pytest

 

-v 输出更详细的信息

包括测试用例的详细结果和其他相关信息,例如测试用例所在的模块,文件路径等

 pytest -v 11pytest

 

-s -v 可以合并到一起写:-sv 或 -vs

 pytest -sv 11pytest

 

-k 筛选用例(模糊匹配)

仅运行匹配的测试用例。指定一个字符串表达式,pytest将根据表达式匹配测试用例的名称来执行相应的测试。-k筛选的不止函数层级,文件中包含同样的关键字也会匹配到。

pytest -sv -k "03" 11pytest  # 仅执行11pytest路径下包含03的测试用例

Python脚本中运行

if __name__ == "__main__":
    pytest.main(['test_01.py', '-vs', '-k', '02'])

 

 

if __name__ == "__main__":
    pytest.main(['test_01.py::Test01::test001', '-vs'])

 

命名规则

测试文件

以test_*.py开头,或"_test.py"结尾。比如:test_login.py

测试类:

以Test开头,利润class TestLogin:

测试函数:

以test_开头,这样pytest会自动识别并执行这些函数作为测试用例。

例如:def test_login_success():或def test_add_product_to_cart()

使用下划线分隔单词

函数名见名知意,例如:test_login_success()测试登录成功的场景

前置和后置

传统的setup和teardown系列(不推荐使用)不如fixture灵活

import pytest

# 模块中的前后置,不能写在测试类中
def setup_module():
    print("模块级别的前置操作")


def teardown_module():
    print("模块级别的后置操作")


def setup_function():
    print("方法级别的前置操作_function")


def teardown_method():
    print("方法级别的后置操作_function")


class Test_login:
    @classmethod  # 类级别需要加classmethod装饰器
    def setup_class(self):
        print("类级别的前置操作")

    @classmethod
    def teardown_class(cls):
        print("类级别的后置操作")


    # 类中的方法前后置为_method,如果不在类中可以写_function
    def setup_method(self):
        print("方法级别的前置操作")


    def teardown_method(self):
        print("方法级别的后置操作")

    def test_case01(self):
        print("test_case01")


if __name__ == '__main__':
    pytest.main("./test_前置后置.py", "-vs")



执行结果:
模块级别的前置操作
类级别的前置操作
方法级别的前置操作
PASSED                         [100%]test_case01
方法级别的后置操作
类级别的后置操作
模块级别的后置操作

 

import pytest

def setup_module():
    print("模块级别的前置操作")


def teardown_module():
    print("模块级别的后置操作")


def setup_function():  # 写在类外面的方法级别的前后置以:_function结尾
    print("方法级别的前置操作_function")


def teardown_function():
    print("方法级别的后置操作_function")


def test_case01():
    print("test_case01")

if __name__ == '__main__':
    pytest.main("./test_前置后置.py", "-vs")



# 执行结果
模块级别的前置操作
方法级别的前置操作_function
PASSED                                     [100%]test_case01
方法级别的后置操作_function
模块级别的后置操作

标签的使用

使用标签可以对测试用例进行分类和分组,通过为标签,选择性的执行特定标签的测试用例,从而方便的控制测试的范围和目标。

下面对标签功能进行演示和说明

1.定义标签的代码

import pytest


@pytest.mark.smoke  # 使用pytest.mark定义标签,smoke为自定义的标签名,可以是任何
def test_login():
    assert True


@pytest.mark.regression  # 定义一个名为regression的标签
def test_regression():
    assert True


@pytest.mark.smoke
def test_add_to_cart():
    assert True


@pytest.mark.regression
def test_checkout():
    assert True

2.在项目目录中创建一个名为pytest.ini的配置文件,名字固定,不可自定义

配置文件中的内容。如果配置文件不生效或者未声明标签,会在执行时报警告

[pytest]
markers =  # 所有的标签在markers下声明
    smoke:Run smoke test  # 对smoke标签进行说明
    regression:Run regression tests

 

3.命令行中执行下面的命令

 pytest 11pytest/test_label.py -m smoke -vs   # -m smoke表示仅执行标签为smoke的测试用例

执行结果仅运行smoke标签标记的测试用例

还可以这样用

pytest -m "smoke or regression" ./11pytest/test_label.py

conftest

conftest.py是一个特殊的文件,管理测试用例中需要通用的数据传递。
conftest的存放位置

project/

├── conftest.py <-- 写公共的 fixture
├── test_login.py <-- 不需要导入 fixture
├── test_order.py
conftest的作用范围:

pytest会从当前测试所在目录开始,向上递归查找所有conftest.py.
如果放一个顶级的conftest用于全局fixture
每个子测试模块也可以放一个conftest.py为该模块生效

@pytest.fixture 装饰器的常用参数

scope=
  "function":(默认值),每个测试函数都会调用一次。通过yield控制前置和后置。yield前为前置执行的代码,yield后为后置要执行的代码,此规则不仅适用于function,其他级别均可以使用yield控制前后置
  "class":每个测试类都会调用一次
  "module":每个测试模块都会调用一次
  "session":每个测试会话都会调用一次

# 代码演示:


import pytest

@pytest.fixture(scope="function")
def setup_teardown():
    print("=== 前置操作 ===")
    yield  # yield 前是前置逻辑,后是后置逻辑
    print("=== 后置操作 ===")


def test_case1(setup_teardown):
    print("执行测试用例 1")

"""
执行结果:
11pytest/config_test/test_conftest.py::test_case1 === 前置操作 ===
执行测试用例 1
PASSED=== 后置操作 ===
"""

 

params:给 fixture 提供多个参数值,每个值都会触发一次测试执行

@pytest.fixture(params=["admin", "user", "guest"])
def role(request):
    return request.param

def test_user(role):
    print(f"当前用户是:{role}")
    assert role in ["admin", "user", "guest"]
执行结果:
11pytest/config_test/test_conftest.py::test_user[admin] 当前用户是:admin
PASSED
11pytest/config_test/test_conftest.py::test_user[user] 当前用户是:user
PASSED
11pytest/config_test/test_conftest.py::test_user[guest] 当前用户是:guest
PASSED

autouse:自动使用fixture,无需在测试函数中显式声明使用该fixture。True和False

@pytest.fixture(autouse=True)  # autouse=True表示每个测试函数执行前都会自动执行这个fixture。即使不在测试函数中引用。
def always_run():
    print("这个 fixture 每个测试都会自动运行")

def test_case1():  # 这里不用引用always_run也会自动执行
    print("执行测试用例 1")

def test_case2():
    print("执行测试用例 2")

"""
执行结果
11pytest/config_test/test_conftest.py::test_case1 这个 fixture 每个测试都会自动运行
执行测试用例 1
PASSED
11pytest/config_test/test_conftest.py::test_case2 这个 fixture 每个测试都会自动运行
执行测试用例 2
PASSED
"""

name:为fixture自定义名称

@pytest.fixture(name="login_token")
def login_fixture():
    return "这是login_fixture"

def test_something(login_token):
    assert login_token == "这是login_fixture"
"""
执行结果:
11pytest/config_test/test_conftest.py::test_something PASSED
"""  

pytest.ini配置文件的使用

一、pytest.ini介绍

避免每次运行都手动输入一堆命令参数,让项目更规范

pytest.inipytest配置文件,用于集中定义项目中的:

  • 启动参数(比如默认加上 -v -s

  • 标记(marks)声明

  • 测试路径规则

  • 忽略的警告

  • 插件配置(如 pytest-html、pytest-cov)

二、pytest.ini的推荐存放路径:项目的根目录

my_project/
├── pytest.ini
├── tests/
│   ├── test_xxx.py
├── src/
│   └── ...

 

 

[pytest]
;addopts:设置默认运行参数(每次运行 pytest 时都生效)
addopts = -v -s --html=report.html --tb=short

;testspath:指定测试用例所在的目录(默认当前目录)
testspath = test_ini

;python_files:指定测试文件的匹配规则,默认是test_*.py
python_files = test_*.py *_test.py

;python_classes:指定测试类的匹配规则,默认是Test*
python_classes = Test*

;python_functions:指定测试函数的匹配规则
python_functions = test_*

;markers:声明项目中用到的mark标签,必须在这里声明才能在使用,否则会报警告
markers =
    smoke:冒烟测试用例
    login:登录模块测试用例

pytest- html生成测试报告

基础数据报告

安装pytest-html

pip install pytest-html

生成测试报告

pytest --html=./report/report.html

 

执行结果

 

放到pytest.ini中 

--self-contained-html表示,将 HTML 报告的所有资源(如 CSS、JS)内联到单个文件中,方便分享。

addopts = -v -s --html=report/report.html --self-contained-html

 

 

 

异常时添加图片

import pytest
from pytest_html import extras


@pytest.hookimpl(hookwrapper=True)  # 写死的
def pytest_runtest_makereport(item, call):  # 写死的
    def image_to_base64(image_path):
        import base64
        """这个函数是将图片转化为base64数据"""
        with open(image_path, "rb") as image_file:
            encoded_string = base64.b64encode(image_file.read())
            return encoded_string.decode('utf-8')

    outcome = yield
    report = outcome.get_result()
    extra = getattr(report, "extra", [])
    if report.when == "call":  # 过滤 前置,和后置,只保留运行中的状态
        print("用例执行结果:", report.outcome)  # 用例执行结果
        if report.outcome != "passed":
            """失败截图数据"""
            image_path = "test_image.jpg"  # 这里你可以换成你的图片路径
            base64_data = image_to_base64(image_path)
            extra.append(extras.image(base64_data))
    report.extra = extra  # 替换数据中的 extra 对象

 

pytest数据驱动

说明:使用不同的测试数据执行相同的测试用例。可以减少测试代码冗余。

第一种数据驱动方案

@pytest.mark.parametrize

import pytest


@pytest.mark.parametrize(
    "input1, input2, expected",
    [
        (1, 2, 3),
        (5, 5, 10),
        (10, -2, 8)
    ]
)
def test_case(input1, input2, expected):
    assert input1 + input2 == expected

 

执行结果

test_数据驱动.py::test_case[1-2-3] PASSED
test_数据驱动.py::test_case[5-5-10] PASSED
test_数据驱动.py::test_case[10--2-8] PASSED

第二种数据驱动方案

# conftest.py
import pytest


@pytest.fixture(params=[
    (1, 2, 3),
    (5, 5, 10),
    (10, -2, 8)],
    name="test_data"
)
def data(request):
    return request.param
# test_case.py

def test_addition(test_data):
    assert test_data[0] + test_data[1] == test_data[2]
# 执行结果
test_case.py::test_addition[test_data0] PASSED
test_case.py::test_addition[test_data1] PASSED
test_case.py::test_addition[test_data2] PASSED

 

posted @ 2025-06-17 21:05  邵杠杠  阅读(40)  评论(0)    收藏  举报