第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会捕获这个异常,然后判断是不是你期望的那个。

总结一下:第一个测试用例,你只需要记住三步:

  1. 写一个以test_开头的函数
  2. 函数里用assert做断言
  3. 在终端运行pytest 文件名.py -v

就这么简单。

好了,这一章的内容就到这里。下一章我们会深入fixture,那是pytest最强大的功能之一。到时候我会分享一些我在项目中用fixture做数据准备和清理的实战经验,保证让你大开眼界。