第3章 pytest入门:核心概念与第一个测试用例
好,咱们正式开始写测试了。
这一章,我会带你从零搭建一个pytest测试环境,然后写出你的第一个测试用例。别担心,内容不多,但都是干货。我个人习惯把最难的东西放在最前面讲——其实pytest的核心概念就那么几个,搞懂了,后面全是套路。
3.1 pytest的核心概念
先说说pytest到底是什么。说白了,它是一个Python测试框架,但比unittest好用太多。我刚开始用unittest的时候,最烦的就是必须写类、继承TestCase、方法名还得对得上……pytest把这些全扔了。
pytest的核心概念,我总结为三个:
- 测试函数:你写一个普通函数,名字以
test_开头就行 - 断言:用Python原生的
assert,不用记一堆assertEqual、assertTrue - fixture:测试前的准备工作和测试后的清理工作,后面章节会细讲
嗯,这里要注意:pytest不强制你用类。你可以把测试写成函数,也可以写成类方法。我个人更推荐函数式写法,简单直接。
3.2 断言机制——为什么assert就够了
你想想看,在unittest里,你要写self.assertEqual(a, b)、self.assertTrue(x)……光记这些方法名就够头疼的。pytest直接让你用Python自带的assert。
举个例子:
# 这是pytest的写法
def test_addition():
result = 1 + 1
assert result == 2
def test_string():
name = "pytest"
assert "py" in name
assert len(name) > 3
是不是很清爽?
我在项目中遇到过一个问题:有个同事用unittest写断言,结果assertEqual和assertAlmostEqual搞混了,浮点数比较一直报错。换成pytest的assert后,直接用assert abs(a - b) < 0.001,一目了然。
核心要点:pytest的断言失败时,会显示详细的上下文信息。比如assert a == b失败,它会告诉你a和b的实际值分别是多少。这一点比unittest强太多。
小技巧:如果你需要断言异常,可以用pytest.raises。比如:
import pytest
def test_raises():
with pytest.raises(ZeroDivisionError):
1 / 0
这样写,既测试了异常,又不用try-except,干净利落。
3.3 测试发现规则——pytest怎么找到你的测试
我第一次用pytest时,最困惑的就是:我写了个文件,pytest怎么知道哪个是测试?
规则其实很简单:
- 文件名:以
test_开头,或者以_test结尾。比如test_demo.py、demo_test.py - 函数名:以
test_开头。比如test_login、test_add_user - 类名:以
Test开头(注意大写T)。比如class TestUser: - 类方法:同样以
test_开头
我曾经踩过一个坑:写了一个test_utils.py文件,里面有个类叫UtilsTest(注意,不是TestUtils),结果pytest死活不跑这个类里的测试。后来才发现,类名必须以Test开头才行。
避坑指南:我曾经因为文件名写成了test_utils.py但放在utils/目录下,而utils/目录没有__init__.py文件,导致pytest找不到测试。记住:pytest默认会递归查找所有符合条件的文件,但目录结构要清晰。
3.4 命令行参数——你必须要掌握的几个
pytest的命令行参数非常多,但常用的就这几个。我列个表,你一看就懂:
| 参数 | 作用 | 示例 |
|---|---|---|
-v |
显示详细信息(verbose) | pytest -v |
-s |
显示print输出(默认不显示) | pytest -s |
-k |
按名称匹配测试用例 | pytest -k "login" |
-x |
遇到第一个失败就停止 | pytest -x |
--maxfail=n |
失败n次后停止 | pytest --maxfail=2 |
--tb=short |
缩短错误堆栈信息 | pytest --tb=short |
我个人最常用的是-v -s组合。调试的时候,加上-x,一有错就停,省得等全部跑完。
小技巧:如果你只想跑某个文件里的某个测试,可以这样:
pytest test_demo.py::test_addition
用::分隔文件名和测试名,精准定位。
3.5 第一个测试用例——动手写吧
好,理论说完了,咱们动手写一个完整的测试用例。
假设我们要测试一个简单的计算器函数:
# calculator.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
现在,我们来写测试文件test_calculator.py:
# test_calculator.py
import pytest
from calculator import add, subtract, multiply, divide
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_subtract():
assert subtract(10, 5) == 5
assert subtract(0, 5) == -5
def test_multiply():
assert multiply(3, 4) == 12
assert multiply(-2, 3) == -6
assert multiply(0, 5) == 0
def test_divide():
assert divide(10, 2) == 5
assert divide(9, 3) == 3
def test_divide_by_zero():
with pytest.raises(ValueError, match="除数不能为0"):
divide(10, 0)
然后在终端运行:
pytest test_calculator.py -v
你会看到类似这样的输出:
============================= test session starts ==============================
collected 5 items
test_calculator.py::test_add PASSED
test_calculator.py::test_subtract PASSED
test_calculator.py::test_multiply PASSED
test_calculator.py::test_divide PASSED
test_calculator.py::test_divide_by_zero PASSED
============================== 5 passed in 0.02s ===============================
看到5个PASSED,是不是很有成就感?
嗯,这里要注意:我故意在test_divide_by_zero里用了pytest.raises来测试异常。为什么?因为如果你不用这个,直接写assert divide(10, 0),程序会直接崩溃,测试也就停了。用pytest.raises,pytest会捕获这个异常,然后判断是不是你期望的那个。
总结一下:第一个测试用例,你只需要记住三步:
- 写一个以
test_开头的函数 - 函数里用
assert做断言 - 在终端运行
pytest 文件名.py -v
就这么简单。
好了,这一章的内容就到这里。下一章我们会深入fixture,那是pytest最强大的功能之一。到时候我会分享一些我在项目中用fixture做数据准备和清理的实战经验,保证让你大开眼界。