← 返回题目列表

Python 测试中如何覆盖异常、边界条件和参数校验?

高频 中等 第 7 / 27 题 更新于 2026/07/27
异常测试边界测试参数校验

简化版

异常和边界测试要覆盖非法输入、空值、边界值、外部依赖失败和权限失败等场景。pytest 中常用 pytest.raises 验证异常,用参数化测试覆盖多组边界数据,避免只测正常路径。

详细版

常见测试点包括:

  • 空字符串、空列表、None;
  • 最大值、最小值、刚好越界;
  • 类型错误、格式错误;
  • 数据不存在、重复数据;
  • 权限不足、未登录;
  • 外部服务超时、数据库异常;
  • 并发或重复提交。

异常测试示例:

import pytest

def test_invalid_age():
    with pytest.raises(ValueError, match="age"):
        create_user(age=-1)

边界测试示例:

@pytest.mark.parametrize("age", [0, 1, 120])
def test_valid_age(age):
    assert validate_age(age) is True

面试要强调:线上问题常常不是正常路径出错,而是边界和异常路径没处理好。

完整版教学

一、为什么不能只测正常路径

很多初级测试只验证“输入正确时返回正确结果”。这当然重要,但真实线上故障常常来自异常路径:参数为空、格式不对、外部服务超时、重复请求、边界值刚好越界。

比如年龄校验,正常测试写:

assert validate_age(18) is True

这只能说明 18 这个输入通过了。它无法证明 -10150None"18" 的处理是正确的。

优秀测试要覆盖正常路径、边界路径和异常路径。

二、异常测试怎么写

pytest 用 pytest.raises 验证异常:

import pytest

def parse_int(value):
    if not value.isdigit():
        raise ValueError("invalid number")
    return int(value)

def test_parse_int_invalid():
    with pytest.raises(ValueError, match="invalid"):
        parse_int("abc")

这里不只是验证会抛 ValueError,还通过 match 验证错误信息包含 invalid。这样可以避免函数抛了别的原因导致的同类型异常也误判通过。

对于业务异常,也可以验证异常对象的字段:

with pytest.raises(BusinessError) as exc:
    pay_order(order)

assert exc.value.code == "BALANCE_NOT_ENOUGH"

三、边界值怎么找

边界值来自业务规则。假设规则是年龄必须在 0 到 120 之间,那么需要测:

  • 最小合法值:0;
  • 最大合法值:120;
  • 刚好低于最小值:-1;
  • 刚好高于最大值:121;
  • 典型合法值:18;
  • 非法类型:None、字符串。

参数化很适合写边界测试:

@pytest.mark.parametrize("age", [0, 18, 120])
def test_valid_age(age):
    assert validate_age(age)

@pytest.mark.parametrize("age", [-1, 121, None])
def test_invalid_age(age):
    with pytest.raises(ValueError):
        validate_age(age)

边界测试的目标不是随机堆很多数据,而是围绕规则边缘选最有代表性的点。

四、参数校验和业务校验要区分

参数校验关注输入格式是否符合要求,例如字段是否缺失、类型是否正确、字符串长度是否超限。业务校验关注输入在业务语义上是否允许,例如库存是否足够、用户是否有权限、订单状态是否可取消。

在 FastAPI 或 Pydantic 项目里,参数格式错误可能由框架自动返回 422;业务规则错误则可能由 service 抛业务异常。

测试时也要分层。API 层测试可以验证 422 响应;service 层测试可以验证业务异常。

五、外部依赖失败怎么测

外部依赖失败是生产高频问题。比如支付接口超时、Redis 不可用、邮件发送失败。可以用 mock 模拟异常:

def test_payment_timeout(mocker):
    mocker.patch("app.payment.client.charge", side_effect=TimeoutError)
    with pytest.raises(PaymentUnavailable):
        pay_order(order_id=1)

这里要验证系统是否做了正确处理,例如重试、降级、记录失败状态、返回合理错误,而不是让底层异常直接泄漏给用户。

六、状态流转的异常分支

订单、任务、审批流这类状态机要特别关注非法状态转换。比如已取消订单不能支付,已支付订单不能重复支付。

测试可以写得非常明确:

def test_cancelled_order_cannot_be_paid():
    order = Order(status="cancelled")
    with pytest.raises(InvalidOrderStatus):
        pay_order(order)

这种测试能防止后续重构时破坏业务规则。

七、常见误区与追问

测试对象典型用例重点断言
边界值0、1、120、121合法边界通过,越界失败
异常ValueError、业务异常异常类型、错误码、错误信息
外部失败超时、限流、连接失败重试、降级、状态回滚
权限失败未登录、非资源所有者401/403 或业务错误码
@pytest.mark.parametrize("age,ok", [(0, True), (120, True), (-1, False), (121, False)])
def test_age_boundary(age, ok):
    assert validate_age(age) is ok

记忆钩子:边界从业务规则推导,不是随机多填几个数;异常要验证“抛了什么”和“为什么抛”。

第一个误区是异常测试只验证“抛异常”,不验证异常类型和错误码。

第二个误区是边界值选择随意。边界应该从业务规则推导,而不是随机填几个数。

第三个误区是 API 层和 service 层混测。不同层次关注点不同,混在一起会让失败原因难定位。

第四个误区是忽略外部依赖失败。真实系统一定会遇到超时、限流、连接失败。

  • 误区:异常测试只要看到抛错就行。 要验证异常类型、错误码、关键错误信息,避免底层无关异常也让测试误通过。
  • 误区:边界值可以随便挑几个数字。 边界应来自规则本身,例如闭区间 [0,120] 就要测 0、120、-1、121 这类刚好贴边的值。
  • 误区:API 层和 service 层所有场景都混在一起测。 API 层更关注状态码和响应结构,service 层更关注业务异常和规则分支,分层更容易定位失败。
  • 追问:pytest.raises(..., match=...) 的价值是什么? 它不仅验证异常类型,还能验证错误信息片段,降低“抛了同类但原因错误”的误判。
  • 追问:外部依赖失败为什么必须测? 网络超时、限流、数据库异常是生产常态,测试能验证系统是否重试、降级、回滚或返回合理错误。
  • 追问:状态机测试的重点是什么? 不只测合法流转,也要测非法流转,例如已取消订单不能支付、已支付订单不能重复扣款。

八、加强记忆

记异常和边界测试:正常路径证明功能能跑,边界路径证明规则守得住,异常路径证明系统扛得住坏情况。pytest 用 raises 验证异常,用参数化覆盖边界,用 mock 模拟外部失败。