没有理想的人不伤心

自动化测试框架 —— Pytest

2025/10/26
3
0

image.png

1. pytest 基础入门

测试框架的功能:

  1. 测试发现
  2. 环境管理
  3. 执行用例
  4. 输入报告

1.1 测试发现

命名规则:

  1. 创建 test_ 开头的文件
  2. 创建 Test 开头的类
  3. 创建 test_ 开头的函数或方法

pytest 以函数/方法作为一个用例,且命名规则必须符合上述规则

命名不符合规则,则为普通函数/方法/类

启动方式:终端输入pytest,会自动执行用例,并输出用例的收集、执行、汇总信息

执行用例终端输出的用例执行情况:
如:

test_first.py .E  
test_first.py  .. 

缩写字母表示对应情况
image.png

1.2 断言 assert

pytest 直接使用 Python 原生的 assert 语句进行断言,无需记忆复杂的断言方法(如 unittest 的 self.assertEqual),更简洁灵活。

# 1. 基础断言
def test_basic_assert():
    assert 10 == 10  # 相等
    assert 20 != 30  # 不等
    assert 5 > 3     # 大于
    assert 2 <= 2    # 小于等于
    assert "hello" in "hello world"  # 包含
    assert "x" not in [1, 2, 3]      # 不包含
    
# 2. 异常断言
def test_exception_assert():
    # 断言:字符串转整数时,非数字字符串会抛ValueError
    with pytest.raises(ValueError):
        int("not_a_number")

    # 进阶:捕获异常对象,验证异常信息
    with pytest.raises(ValueError) as exc_info:
        int("invalid")
    assert "invalid literal for int()" in str(exc_info.value)  # 验证异常描述
    
# 3. 布尔值断言
def test_boolean_assert():
    assert True  # 断言条件为真
    assert not False  # 断言条件为假
    assert len([1, 2, 3]) > 0  # 间接断言(列表非空)
    
# 4. 近似值断言
def test_approx_assert():
    # 直接比较可能失败(因浮点数精度)
    # assert 0.1 + 0.2 == 0.3  # 实际结果是0.30000000000000004,会失败
    
    # 用approx()允许微小误差
    assert 0.1 + 0.2 == pytest.approx(0.3)  # 成功
    
# 5. 集合/列表断言
def test_collection_assert():
    # 断言列表包含指定元素(不考虑顺序)
    assert [1, 2, 3] == pytest.assertion.rewrite.util.same_elements([3, 2, 1])
    
    # 断言集合相等
    assert {1, 2, 3} == {3, 2, 1}

1.3 前置后置处理(setup/teardown)

用于在测试前准备资源(如初始化数据)、测试后清理资源(如关闭连接),pytest 支持函数级和类级的前后置:

  1. 函数级(每个测试函数执行前后)
def setup_function():
    print("\n测试函数开始前:准备数据")

def teardown_function():
    print("测试函数结束后:清理数据")

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

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

运行结果(加 -s 查看 print):

测试函数开始前:准备数据
执行测试用例1
测试函数结束后:清理数据

测试函数开始前:准备数据
执行测试用例2
测试函数结束后:清理数据
  1. 类级(每个测试类中的所有用例执行前后)
class TestDB:
    @classmethod
    def setup_class(cls):
        print("\n测试类开始前:连接数据库")

    @classmethod
    def teardown_class(cls):
        print("测试类结束后:关闭数据库")

    def test_query(self):
        print("执行查询测试")

    def test_insert(self):
        print("执行插入测试")

运行结果:

测试类开始前:连接数据库
执行查询测试
执行插入测试
测试类结束后:关闭数据库

这里的 @classmethod 是一个装饰器,等同于类的静态方法,第一个参数固定为 cls,代表类本身,通过cls,可以访问类的属性和其他类方法,常用于定义与类相关的工具方法

1.4 fixture 测试资源的灵活管理

Fixture 用于定义测试前的准备(如初始化资源)和测试后的清理(如释放资源),替代传统的 setup/teardown,支持跨用例、跨模块共享,是自动化测试中管理公共逻辑的核心工具。

核心特性:

  • 作用域(scope):控制 fixture 的执行次数,可选值:

    • function(默认,每个用例执行一次)
    • class(每个类执行一次,类内测试共享)
    • module(每个测试文件执行一次,文件内测试共享)
    • session(整个测试会话调用一次,全局共享)。
  • 依赖传递:fixture 可以依赖其他 fixture,形成依赖链。

  • 自动使用(autouse):设置 autouse=True 时,作用域内的所有用例会自动调用该 fixture,无需显式声明。

  • 参数化(params):通过 params 为 fixture 传入多组参数,配合 request 对象获取参数,实现多场景复用。

1.5 参数化

通过 @pytest.mark.parametrize 为测试用例传入多组数据,实现 “一套逻辑,多组数据” 的测试,尤其适合接口测试、表单验证等场景。

进阶用法

  • 多参数组合:同时对多个参数进行参数化(如接口的 method 和 data 组合)。
  • 从外部文件读取数据:结合 yaml/json 等文件,实现测试数据与代码分离。

1.6 标记与筛选

通过 @pytest.mark.xxx 为用例打标记(如 smoke 冒烟用例、regression 回归用例、prod 生产环境用例),运行时通过 -m 筛选指定标记的用例,灵活控制执行范围。

通过 mark,可以实现对测试用例的分类、筛选、条件执行等功能,灵活控制测试范围(例如只运行 “冒烟测试” 用例,或跳过某些暂时不执行的用例)。

自定义标记需在项目根目录的 pytest.ini 中注册(否则运行时会警告 “未知标记”):

[pytest]
markers =
    smoke: 冒烟测试用例(核心功能)
    regression: 回归测试用例
    payment: 支付模块用例
    high: 高优先级
    low: 低优先级

1.7 生态扩展

pytest 的强大很大程度依赖于插件,自动化测试中常用插件:

  • pytest-html:生成美观的 HTML 测试报告(含用例详情、失败截图等)。
  • pytest-xdist:多进程并行执行用例,大幅缩短执行时间(尤其适合用例量大的场景)。
  • pytest-rerunfailures:失败用例自动重试(解决网络波动、偶发超时等问题)。
  • pytest-ordering:控制用例执行顺序(虽然不推荐强依赖,但特殊场景下有用,如流程性测试)。

2. 具体案例

2.1 接口自动化测试(结合 Fixture 和参数化)

场景:测试一个用户登录接口(POST /api/login),需要覆盖 “正确账号密码”“账号不存在”“密码错误” 等场景,且后续接口需依赖登录返回的 token

实现步骤

  1. 用 fixture 管理公共资源(如请求会话、基础 URL、清理测试数据)。
  2. 用参数化覆盖多组测试数据。
  3. 用 fixture 传递 token 给后续依赖接口。
import pytest
import requests
import json

# 读取测试数据(从外部json文件)
with open("login_data.json", "r") as f:
    login_cases = json.load(f)  
    # 格式:[{"case": "正确账号", "data": {...}, "expected": {...}}, ...]

# Fixture:全局会话(复用请求连接)
@pytest.fixture(scope="session")
def session():
    s = requests.Session()
    yield s
    s.close()  # 测试结束后关闭会话

# Fixture:基础URL(从配置文件读取,这里简化)
@pytest.fixture(scope="session")
def base_url():
    return "https://api-test.example.com"

# Fixture:登录并返回token(依赖session和base_url)
@pytest.fixture(scope="module")
def login_token(session, base_url):
    # 前置:执行登录(用正确账号,为后续接口准备token)
    login_data = {"username": "test_user", "password": "test_pass"}
    res = session.post(f"{base_url}/api/login", json=login_data)
    assert res.status_code == 200
    token = res.json()["data"]["token"]
    yield token
    # 后置:登出(清理状态)
    session.post(f"{base_url}/api/logout", headers={"Authorization": f"Bearer {token}"})

# 测试登录接口(参数化多场景)
@pytest.mark.parametrize("case", login_cases)
def test_login(session, base_url, case):
    res = session.post(
        f"{base_url}/api/login",
        json=case["data"]
    )
    # 断言状态码和响应信息
    assert res.status_code == case["expected"]["status_code"]
    assert res.json()["message"] == case["expected"]["message"]

# 测试依赖token的接口(如获取用户信息)
def test_get_user_info(session, base_url, login_token):
    res = session.get(
        f"{base_url}/api/user/info",
        headers={"Authorization": f"Bearer {login_token}"}
    )
    assert res.status_code == 200
    assert res.json()["data"]["username"] == "test_user"

核心亮点

  • session fixture 复用 HTTP 连接,减少开销;
  • login_token fixture 前置登录、后置登出,确保测试环境干净;
  • 从外部文件读取测试数据,实现 “数据 - 代码” 分离,便于维护。

2.2 UI 自动化测试

场景:用 Selenium 测试某网站的搜索功能,需要支持失败重试、并行执行,并生成带截图的报告。

实现步骤

  1. 用 fixture 管理浏览器实例(启动 / 关闭)。
  2. 用 pytest-rerunfailures 处理偶发失败(如元素加载慢)。
  3. 用 pytest-xdist 并行执行多浏览器用例。
  4. 失败时自动截图并嵌入 HTML 报告。
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Fixture:浏览器驱动(支持多浏览器参数化)
@pytest.fixture(scope="function", params=["chrome", "firefox"])
def driver(request):
    browser = request.param
    if browser == "chrome":
        driver = webdriver.Chrome()
    else:
        driver = webdriver.Firefox()
    driver.maximize_window()
    yield driver
    driver.quit()  # 每个用例结束后关闭浏览器

# 失败时自动截图(结合pytest-html)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
    outcome = yield
    report = outcome.get_result()
    if report.when == "call" and report.failed:
        # 获取driver实例
        driver = item.funcargs.get("driver")
        if driver:
            # 截图并保存到报告
            screenshot = driver.get_screenshot_as_base64()
            report.extra = [pytest_html.extras.image(screenshot, "失败截图")]

# 测试搜索功能
@pytest.mark.smoke  # 标记为冒烟用例
def test_search(driver):
    driver.get("https://example.com")
    # 等待搜索框加载
    search_box = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "search-input"))
    )
    search_box.send_keys("pytest")
    driver.find_element(By.ID, "search-btn").click()
    # 断言结果
    assert "pytest" in driver.title