写测试不是目的,用合适的测试把质量守住才是目的。在实际项目中,通常会搭配三种不同粒度的测试:单元测试、集成测试和接口测试。它们各司其职,构成一个“测试金字塔”。
单元测试
- 定义:对软件中最小的可测试单元(通常是一个函数或一个类方法)进行隔离验证,不依赖外部系统(数据库、网络、文件系统)。
- 目的:快速、精确地定位逻辑错误,保证每一块“积木”本身的正确性。
- Python 中的典型做法:
- 用
pytest写测试函数,文件名以test_开头,函数名也以test_开头。 - 使用
assert语句直接断言结果,遇到外部依赖时用unittest.mock或pytest-mock进行 Mock。 - 简单示例:
# 被测代码:计算折扣价
def apply_discount(price, discount):
if not 0 <= discount <= 1:
raise ValueError("折扣率需在0到1之间")
return price * (1 - discount)
# 单元测试
def test_apply_discount():
assert apply_discount(100, 0.2) == 80.0
def test_invalid_discount():
import pytest
with pytest.raises(ValueError):
apply_discount(100, 1.5)
- 执行速度:极快(毫秒级),应在每次提交代码前全量跑通。
集成测试
- 定义:验证多个组件协同工作是否正常,通常会涉及真实的依赖,比如数据库、消息队列、缓存等。
- 目的:发现单元测试覆盖不到的模块间交互问题,例如 SQL 语法错误、序列化/反序列化不匹配、事务回滚逻辑等。
- 与单元测试的关键区别:不 Mock 关键依赖,而是连接一个真实(或类生产)的环境,比如本地测试数据库、内存中的 Redis 等。
- Python 中的实现思路:
- 利用
pytest的 fixture 来准备和清理测试环境(如创建临时表、启动测试容器)。 - 测试一个完整的业务流程(如“用户注册→登录→获取个人信息”),检验组件之间的数据流转是否正确。
- 示例结构(伪代码):
@pytest.fixture
def db_connection():
# 创建一个测试数据库连接,或在用 Docker 时启动一个临时容器
conn = create_test_db()
yield conn
conn.close()
def test_user_registration_and_login(db_connection):
user = register(db_connection, "testuser", "pwd123")
assert user.id is not None
token = login(db_connection, "testuser", "pwd123")
assert token is not None
- 注意点:集成测试比单元测试慢,且依赖外部环境,需要保证环境可重复(如使用 Docker Compose 拉起测试依赖)。
接口测试
- 定义:针对系统的对外接口(通常是 HTTP API)进行端到端的请求-响应验证,检查状态码、响应体、Header 等是否符合预期。
- 目的:从调用方角度验证整个服务的功能、容错和协议约定,确保接口契约不会被内部重构破坏。
- 常见工具:
- Python 生态中,可以用
requests配合pytest直接发 HTTP 请求并断言。 - 也可以使用专门工具,如
pytest+httpx(支持异步)、Tavern(声明式接口测试)或 Postman/Newman。 - 示例:
import requests
def test_get_user_api():
response = requests.get("http://localhost:8000/users/1")
assert response.status_code == 200
data = response.json()
assert "id" in data
assert "name" in data
- 测试范围:通常覆盖完整的请求链路(路由、中间件、业务逻辑、数据库),但可以把外部服务(如第三方支付)Mock 掉,避免产生真实费用或不稳定依赖。
三者如何配合
测试金字塔原则给出了清晰的指导:
- 大量单元测试:稳定、快速、维护成本低,覆盖所有核心逻辑和边界条件。
- 适量集成测试:验证关键模块组合和外部依赖交互,保证组件协作不出问题。
- 少量接口测试:验证最重要、最常用的业务场景和 API 契约,也可以作为冒烟测试。
在 Python 项目中,这三类测试通常都放在同一个 tests/ 目录下,通过 pytest 统一运行。通过合理的分类(如用 mark 标记或独立目录),可以在 CI/CD 中灵活执行:提交代码时跑单元测试,PR 合并前跑集成测试,部署后跑接口冒烟测试。这样就能在保证质量的同时,避免测试耗时过长,让开发反馈始终保持高效。