📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)

📝 职场经验干货:

软件测试工程师简历上如何编写个人信息(一周8个面试)

软件测试工程师简历上如何编写专业技能(一周8个面试)

软件测试工程师简历上如何编写项目经验(一周8个面试)

软件测试工程师简历上如何编写个人荣誉(一周8个面试)

软件测试行情分享(这些都不了解就别贸然冲了.)

软件测试面试重点,搞清楚这些轻松拿到年薪30W+

软件测试面试刷题小程序免费使用(永久使用)


今天,我想和大家深入聊聊 Pytest 框架中一个极其核心且强大的特性——断言(Assertions)。

本文将带你从基础到进阶,结合 API 自动化测试的实战场景,并探讨如何在日常工作中更高效地运用Pytest 断言。

断言:测试的核心使命

在软件测试中,无论手动还是自动,其根本目的都是验证实际结果(Actual Result)是否与预期结果(Expected Result)一致。

这个“验证”动作,在自动化测试脚本中,就是通过断言来完成的。

一个没有断言的测试用例,充其量只能算是一个执行流程,无法判断其执行结果的正确性,也就失去了测试的意义。

传统的 unittest 框架(以及许多其他语言的类似框架)通常提供一系列 assertXxx 方法,

例如 assertEqual, assertTrue, 

assertIn, assertRaises 等。

这种方式虽然明确,但有时显得冗长,且记忆负担较重。

# unittest 风格示例
import unittest

class MyUnittest(unittest.TestCase):
    def test_addition(self):
        result = 2 + 2
        self.assertEqual(result, 4, "加法结果应为 4") # 需要使用特定的 assertEqual 方法
        self.assertTrue(result > 0, "结果应为正数") # 需要使用 assertTrue

    def test_dict_contains(self):
        my_dict = {'a': 1, 'b': 2}
        self.assertIn('a', my_dict, "字典应包含键 'a'") # 需要使用 assertIn

而 Pytest 的设计哲学崇尚简洁与直观,它巧妙地利用了 Python 内置的 assert 语句。

Pytest 断言:

assert 语句的威力

Pytest 最显著的特点之一就是可以直接使用 Python 标准的 assert 关键字进行断言。

这不仅让代码更符合简洁的风格,也大大降低了学习和使用的门槛。

# Pytest 风格示例
import pytest

def test_addition_pytest():
    result = 2 + 2
    assert result == 4  # 直接使用 assert
    assert result > 0

def test_dict_contains_pytest():
    my_dict = {'a': 1, 'b': 2}
    assert 'a' in my_dict # 同样直接使用 assert

当使用 Pytest 运行此测试时,你将看到以下的输出:

看到这里,你可能会问:“这不就是普通的 Python assert 吗?它失败时只会抛出 AssertionError,信息量有限,调试起来岂不是很困难?”

这正是 Pytest 的巧妙之处!Pytest 在执行测试时,会对 assert 语句进行智能重写(Assertion Rewriting。

当断言失败时,Pytest 不仅仅是抛出一个简单的 AssertionError,而是会进行深入的内省(Introspection),提供极其丰富和清晰的上下文对比信息,极大地帮助我们定位问题。

假设我们有如下错误的断言:

def test_list_comparison():
    list1 = [1, 2, 3, 4]
    list2 = [1, 2, 5, 4]
    assert list1 == list2

当使用 Pytest 运行此测试时,你将看到以下的输出:

  

看到了吗?Pytest 不仅告诉你断言失败了,还精确地指出了列表在索引 2 处不同(3 != 5),并给出了清晰的 diff 对比。

这种详细的失败信息对于调试复杂的数据结构(如长列表、嵌套字典、对象属性等)至关重要,远胜于 unittest 中 assertEqual 失败时可能只打印两个完整列表的输出。

Pytest 断言的优势总结:

1.  简洁直观:

使用标准 assert,代码更 Pythonic,易于读写。

2.  低学习成本:

无需记忆大量 assertXxx 方法名。

3.  强大的失败信息:

通过断言重写和内省,提供极其丰富的上下文和 diff 对比,加速问题定位。

4.  通用性:

几乎可以用 assert 验证任何 Python 表达式的真值。

Pytest 断言在 API 自动化

测试中的实战应用

API 自动化测试是 Pytest 应用最广泛的领域之一。

一个典型的 API 测试流程通常包括:发送请求 -> 接收响应 -> 验证响应。

断言在“验证响应”这一步扮演着核心角色。

假设我们正在测试一个用户管理 API,使用 requests 库发送 HTTP 请求。

场景一:

验证获取用户信息接口 (GET /users/{id})​​​​​​​

import requests
import pytest

BASE_URL = "https://api.example.com/v1"
def test_get_user_details_success():
    """测试成功获取用户详情"""
    user_id = 123
    expected_username = "john_doe"
    expected_email = "john.doe@example.com"

    response = requests.get(f"{BASE_URL}/users/{user_id}")

01 断言:HTTP 状态码​​​​​​​

    # 这是最基本也是最重要的断言之一
    assert response.status_code == 200, f"请求 /users/{user_id} 预期状态码 200, 实际为 {response.status_code}"
    # Pytest assert 支持可选的失败消息,对于动态内容尤其有用

02 断言:响应头 (Content-Type)​​​​​​​

# 确保返回的是预期的 JSON 格式
    assert 'application/json' in response.headers.get('Content-Type', ''), \
           f"响应头 Content-Type 预期包含 'application/json', 实际为 {response.headers.get('Content-Type')}"

03 断言:响应体 JSON 数据​​​​​​​

try:
        data = response.json()
    except requests.exceptions.JSONDecodeError:
        pytest.fail(f"响应体不是有效的 JSON 格式: {response.text}") # 使用 pytest.fail 强制失败

3.1 断言:关键字段是否存在​​​​​​​

assert 'id' in data, "响应 JSON 中缺少 'id' 字段"
assert 'username' in data, "响应 JSON 中缺少 'username' 字段"
assert 'email' in data, "响应 JSON 中缺少 'email' 字段"

3.2 断言:字段值是否符合预期​​​​​​​

assert data['id'] == user_id, f"响应 'id' 预期为 {user_id}, 实际为 {data.get('id')}"
assert data['username'] == expected_username, \
       f"响应 'username' 预期为 '{expected_username}', 实际为 '{data.get('username')}'"
assert data['email'] == expected_email, \
       f"响应 'email' 预期为 '{expected_email}', 实际为 '{data.get('email')}'"

3.3 断言:字段类型

assert isinstance(data['id'], int), f"'id' 字段类型预期为 int, 实际为 {type(data.get('id'))}"
assert isinstance(data['is_active'], bool), f"'is_active' 字段类型预期为 bool, 实际为 {type(data.get('is_active'))}" # 假设有此字段

3.4 断言:列表/嵌套结构 (假设用户有订单列表)​​​​​​​

    assert 'orders' in data, "响应 JSON 中缺少 'orders' 字段"
    assert isinstance(data['orders'], list), f"'orders' 字段类型预期为 list, 实际为 {type(data.get('orders'))}"
    if data.get('orders'): # 如果列表不为空
        assert 'order_id' in data['orders'][0], "订单对象中缺少 'order_id'"
        assert isinstance(data['orders'][0]['total_amount'], (int, float)), "'total_amount' 应为数字类型"

def test_get_user_not_found():
    """测试获取不存在的用户"""
    non_existent_user_id = 99999

    response = requests.get(f"{BASE_URL}/users/{non_existent_user_id}")

    # 断言:状态码为 404 Not Found
    assert response.status_code == 404, f"请求不存在的用户预期 404, 实际 {response.status_code}"

    # 断言:错误响应体 (假设有统一的错误结构)
    try:
        error_data = response.json()
        assert 'error_code' in error_data, "错误响应中缺少 'error_code'"
        assert error_data.get('error_code') == 'USER_NOT_FOUND', \
               f"错误码预期 'USER_NOT_FOUND', 实际 '{error_data.get('error_code')}'"
        assert 'message' in error_data, "错误响应中缺少 'message'"
        assert f"User with id {non_existent_user_id} not found" in error_data.get('message', ''), \
               "错误消息内容不符合预期"
    except requests.exceptions.JSONDecodeError:
        pytest.fail("404 响应体不是有效的 JSON 格式")

落地实践思考:

标准化断言

对于常见的验证点(如状态码、标准错误结构),可以封装成辅助函数或 Pytest 自定义断言,提高代码复用性和可维护性。​​​​​​​

# 辅助函数示例
    def assert_api_success(response: requests.Response, expected_status_code: int = 200):
        assert response.status_code == expected_status_code, \
            f"预期状态码 {expected_status_code}, 实际 {response.status_code}. Response: {response.text[:200]}" # 加上部分响应体便于调试
        assert 'application/json' in response.headers.get('Content-Type', ''), "Content-Type 应为 JSON"

    def assert_api_error(response: requests.Response, expected_status_code: int, expected_error_code: str):
        assert response.status_code == expected_status_code, f"预期错误状态码 {expected_status_code}, 实际 {response.status_code}"
        try:
            error_data = response.json()
            assert error_data.get('error_code') == expected_error_code, \
                   f"预期错误码 '{expected_error_code}', 实际 '{error_data.get('error_code')}'"
        except (requests.exceptions.JSONDecodeError, KeyError):
             pytest.fail(f"无法解析或找到预期的错误结构. Response: {response.text[:200]}")

    # 在测试中使用
    def test_get_user_success_refactored():
        response = requests.get(f"{BASE_URL}/users/123")
        assert_api_success(response) # 使用辅助函数简化基础验证
        data = response.json()
        # ... 其他特定于此接口的断言 ...
 
    def test_get_user_not_found_refactored():
        response = requests.get(f"{BASE_URL}/users/99999")
        assert_api_error(response, 404, 'USER_NOT_FOUND') # 使用辅助函数

数据驱动测试

结合 Pytest 的 

@pytest.mark.parametrize,可以用同一套断言逻辑验证多种输入和预期输出,尤其适用于测试创建(POST)或更新(PUT/PATCH)接口。​​​​​​​

    @pytest.mark.parametrize("payload, expected_status, expected_username", [
        ({"username": "new_user", "email": "new@example.com", "is_active": True}, 201, "new_user"),
        ({"username": "another_user", "email": "another@example.com"}, 201, "another_user"), # 假设 is_active 默认为 false
        ({"email": "missing_username@example.com"}, 400, None), # 假设 username 是必需的
    ])
    def test_create_user(payload, expected_status, expected_username):
        response = requests.post(f"{BASE_URL}/users", json=payload)

        assert response.status_code == expected_status

        if expected_status == 201:
            assert_api_success(response, expected_status_code=201)
            data = response.json()
            assert data['username'] == expected_username
            assert 'id' in data # 确保创建成功返回了 ID
        elif expected_status == 400:
            assert_api_error(response, 400, 'VALIDATION_ERROR') # 假设验证错误的 error_code
            # 可以进一步断言错误消息细节,比如指出哪个字段缺失
            assert 'username' in response.json().get('details', {}).get('field_errors', {}), \
                   "错误详情应指出 'username' 字段问题"

Pytest 断言进阶技巧与特殊场景

Pytest 的 assert 不仅仅能做简单的相等性比较,它还可以优雅地处理更多复杂场景。

4.1 断言异常 (pytest.raises)

当你需要验证某段代码是否按预期抛出了特定类型的异常时,pytest.raises 是标准做法。​​​​​​​

import pytest

def my_risky_function(value):
    if not isinstance(value, int):
        raise TypeError("输入必须是整数")
    if value <= 0:
        raise ValueError("输入必须是正整数")
    return value * 2

def test_risky_function_raises_type_error():
    with pytest.raises(TypeError): # 断言代码块会抛出 TypeError
        my_risky_function("not an integer")

def test_risky_function_raises_value_error_with_message():
    with pytest.raises(ValueError, match="必须是正整数"): # 断言 ValueError,并匹配异常消息 (使用正则表达式)
        my_risky_function(0)

    # 也可以获取异常对象进行更详细的断言
    with pytest.raises(ValueError) as excinfo:
        my_risky_function(-5)
    assert "正整数" in str(excinfo.value) # 断言异常对象的字符串表示包含特定文本
    # excinfo.value 是捕获到的异常实例

应用场景:

测试函数参数校验、边界条件处理、资源不存在时的预期异常等。

在 API 测试中,可以用来验证无效输入导致的特定业务异常(如果 API 层设计为抛出 Python 异常而不是仅返回错误码)。

4.2 断言集合、字典的部分内容

有时我们不关心完整的集合或字典,只想确认某些关键元素或键值对存在。​​​​​​​

def test_set_subset():
    my_set = {1, 2, 3, 4, 5}
    expected_subset = {1, 3, 5}
    assert expected_subset.issubset(my_set) # 明确使用 issubset 方法
    # 或者更 Pythonic 的方式 (Pytest 同样能提供好的失败信息)
    assert {1, 3, 5} <= my_set

def test_dict_contains_items():
    my_dict = {'id': 101, 'name': 'widget', 'price': 99.9, 'status': 'active'}
    expected_items = {'name': 'widget', 'status': 'active'}

    # 方法一:逐个断言
    assert my_dict['name'] == 'widget'
    assert my_dict['status'] == 'active'

    # 方法二:使用字典视图比较 (Python 3) - 更简洁,Pytest 能很好地处理失败信息
    # 注意:这要求值也完全匹配
    assert expected_items.items() <= my_dict.items()

应用场景:

当 API 响应体很大,但我们只关心其中几个关键字段的值时,或者验证数据库查询结果是否包含某些特定记录时。

4.3 软断言

默认情况下,Pytest 测试函数在遇到第一个失败的 assert 时就会停止执行。

但在某些场景下(尤其是复杂的 API 响应验证),我们可能希望执行完所有断言,收集所有失败信息,而不是在第一个失败处就停下来。这被称为“软断言”或“继续执行断言”。

Pytest 本身不直接内置软断言,但可以通过插件 pytest-assume 或 pytest-check 来实现。​​​​​​​

# 使用 pytest-assume 示例 (需要 pip install pytest-assume)
from pytest_assume.plugin import assume
import pytest

def test_multiple_assertions_with_assume():
    response_data = {'name': 'Test Product', 'price': 100, 'stock': -5} # 假设 stock 不应为负

    with assume: assert response_data['name'] == 'Test Product' # 通过
    with assume: assert response_data['price'] == pytest.approx(99.9) # 失败 1: 价格不符
    with assume: assert response_data['stock'] > 0, f"库存应为正数, 实际 {response_data['stock']}" # 失败 2: 库存为负

    # 测试会继续执行到这里,即使前面的 assume 失败了
    print("测试函数执行完毕")
    # Pytest 的报告会列出所有失败的 assume 断言

应用场景与落地思考:

何时使用?

当一个测试用例需要验证一个对象的多个独立属性,或者一个 API 响应的多个不同方面时,软断言可以一次性暴露所有问题,提高调试效率。

例如,验证一个复杂表单提交后的所有字段回显是否正确。

谨慎使用

过度使用软断言可能掩盖测试用例设计的缺陷。

如果多个断言之间存在逻辑依赖(例如,先断言用户存在,再断言用户属性),则不应使用软断言。

一个测试用例最好还是聚焦于验证一个具体的行为或需求点。软断言更适用于验证同一行为/对象产生的多个并行且独立的预期结果。

当前工作结合

在我们的 API 测试中,如果一个接口返回非常复杂的 JSON 结构,包含用户的基本信息、地址列表、订单历史等,使用软断言可以同时检查这几个部分的关键信息是否符合预期,而不用因为地址格式错误就忽略了订单历史的检查。

4.4 自定义断言消息

虽然 Pytest 的自动内省通常足够好,但在某些复杂逻辑或边界条件下,添加自定义的失败消息可以提供更多业务层面的上下文。​​​​​​​

def test_complex_condition_with_message():
    calculated_value = calculate_complex_thing(...)
    threshold = get_threshold_for_user_level(...)
    user_level = "premium"

    assert calculated_value >= threshold, \
        f"对于 {user_level} 用户, 计算值 {calculated_value} 未达到阈值 {threshold}"

最佳实践

只有在 Pytest 自动生成的消息不够清晰,或者需要补充特定业务逻辑上下文时才使用自定义消息。滥用自定义消息会掩盖 Pytest 强大的 diff 能力。

编写优秀 Pytest 

断言的最佳实践

1.  保持断言的原子性与清晰性:

  • 一个测试用例,一个逻辑断言点(理想情况):

    尽量让每个测试函数聚焦于验证一个具体的行为或需求。

    虽然 API 测试中可能需要对一个响应进行多方面断言,但这些断言应共同服务于“验证该响应是否正确”这一核心目标。

  • 断言应简单直接:

    避免在 assert 语句中进行复杂的计算或逻辑判断。应先计算出实际值和预期值,再用 assert 进行比较。​​​​​​​

# 不推荐
assert calculate_discount(price, user_level) * quantity < get_budget_limit()

# 推荐
actual_total = calculate_discount(price, user_level) * quantity
budget_limit = get_budget_limit()
assert actual_total < budget_limit, f"总价 {actual_total} 超出预算 {budget_limit}"

2.  利用 Pytest 的内省能力:

相信 Pytest 的 diff 输出。优先使用简单的 assert actual == expected,而不是手动去构造复杂的错误消息。

3.  使用恰当的比较方式:

  • 浮点数用 pytest.approx。

  • 异常用 pytest.raises。

  • 成员关系用 in (assert item in container)。

  • 类型检查用 isinstance() 

    (assert isinstance(obj, ExpectedType))。

  • 布尔真值直接断言 (assert is_valid, assert not is_error)。

4.  封装通用断言逻辑:

对于跨多个测试用例重复出现的断言模式(如 API 响应状态码和基本结构验证),封装成可复用的辅助函数或 Pytest 钩子、自定义断言库,遵循 DRY (Don't Repeat Yourself) 原则。

5.  理解断言失败的输出:

学会阅读 Pytest 提供的详细失败报告。

-v(详细模式)和 -vv(更详细模式)参数可以提供更多信息。

--showlocals (或 -l) 可以显示失败断言处的局部变量值。

6.  结合 Fixtures 使用断言:

Fixtures 可以用来准备测试数据或环境。

有时,断言也可以用在 Fixture 内部(例如,断言 setup 成功),或者更常见的是,Fixture 返回的数据被测试函数中的断言所使用。

调试断言失败

当断言失败时,除了依赖 Pytest 的输出,还可以:

  • 使用 print() 或 logging:在断言前后打印相关变量的值,帮助理解上下文。

  • 使用调试器:在断言失败处设置断点。Pytest 与 pdb(Python Debugger)及 IDE 的调试器(如 PyCharm, VS Code)集成良好。可以使用 --pdb 命令行选项让 Pytest 在失败或错误时自动进入 pdb。

  • 简化测试用例:暂时注释掉部分代码或断言,缩小问题范围。

结语

Pytest 的断言机制是其简洁、高效和强大特性的集中体现。通过拥抱原生的 assert 语句,并辅以智能的内省和丰富的辅助工具,Pytest 极大地提升了我们编写和维护自动化测试的体验和效率。

在 API 自动化测试的实践中,熟练运用各种断言技巧,结合辅助函数、参数化、软断言等策略,能够构建出既健壮又易于理解的测试套件。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】

​​​

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐