第四章 单元测试框架:unittest入门、pytest基础、测试用例编写规范

说实话,很多刚入行的同学对「单元测试」这件事有点抵触。觉得写测试比写代码还麻烦,浪费时间。我以前也这么想,直到有一次我重构了一个排序算法,改完发现结果全乱了——嗯,那次我花了整整一个下午手动验证,最后还漏了一个边界条件。从那以后,我再也不敢不写测试了。

这一章,我们就来聊聊 Python 里最常用的两个测试框架:unittest 和 pytest。说白了,它们就是帮你自动验证代码正确性的工具。你写一次测试,以后每次改代码都能一键跑完,省心省力。

4.1 unittest 入门:Python 自带的测试框架

unittest 是 Python 标准库的一部分,不需要额外安装。它的设计思路借鉴了 Java 的 JUnit,所以如果你写过 Java 测试,上手会非常快。

我个人习惯用 unittest 写一些比较正式的测试场景,比如算法验证、接口测试。它的结构很清晰:测试类、测试方法、断言方法,三件套。

4.1.1 基本用法

来看一个最简单的例子。假设我们要测试一个加法函数:

# calculator.py
def add(a, b):
    return a + b

# test_calculator.py
import unittest
from calculator import add

class TestCalculator(unittest.TestCase):
    def test_add_positive(self):
        self.assertEqual(add(1, 2), 3)

    def test_add_negative(self):
        self.assertEqual(add(-1, -2), -3)

    def test_add_zero(self):
        self.assertEqual(add(0, 0), 0)

if __name__ == '__main__':
    unittest.main()

运行方式很简单:python test_calculator.py。你会看到类似这样的输出:

...
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

三个点代表三个测试都通过了。如果某个测试失败,会显示 F 并给出详细的错误信息。

小技巧: 测试方法名必须以 test_ 开头,否则 unittest 不会识别。我刚开始写的时候经常忘记这个规则,结果测试跑了个寂寞。

4.1.2 常用断言方法

unittest 提供了很多断言方法,我列几个最常用的:

断言方法 作用
assertEqual(a, b) 检查 a == b
assertTrue(x) 检查 x 为 True
assertFalse(x) 检查 x 为 False
assertRaises(Error, func, args) 检查是否抛出指定异常
assertAlmostEqual(a, b, places) 检查浮点数近似相等(指定小数位)

举个例子,验证一个除法函数:

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为0")
    return a / b

class TestDivide(unittest.TestCase):
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5)

    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

这里用 assertRaises 来验证异常,非常实用。我曾经在写二分查找算法时,就靠这个捕获了数组越界的 bug。

4.2 pytest 基础:更简洁、更强大

pytest 是第三方库,需要安装:pip install pytest。它比 unittest 更简洁,写起来更像普通函数,不需要继承任何类。

我个人更喜欢用 pytest 做日常开发中的测试,因为它少写很多样板代码。你想想看,unittest 里每个测试都要写 self.assertEqual,pytest 直接用 Python 原生的 assert 就行。

4.2.1 基本用法

同样的加法测试,用 pytest 写是这样的:

# test_calculator_pytest.py
from calculator import add

def test_add_positive():
    assert add(1, 2) == 3

def test_add_negative():
    assert add(-1, -2) == -3

def test_add_zero():
    assert add(0, 0) == 0

运行命令:pytest test_calculator_pytest.py。输出更简洁:

============================= test session starts ==============================
collected 3 items

test_calculator_pytest.py ...                                          [100%]

============================== 3 passed in 0.02s ===============================

看到没?不需要写类,不需要写 unittest.main(),直接写函数就行。pytest 会自动发现所有以 test_ 开头的函数。

核心区别: unittest 是「类 + 方法」风格,pytest 是「函数 + assert」风格。pytest 更 Pythonic,写起来更爽。

4.2.2 参数化测试

这是 pytest 的一个杀手锏。假设你要测试一个函数在不同输入下的表现,unittest 需要写多个方法,pytest 可以用 @pytest.mark.parametrize 一行搞定:

import pytest

def is_even(n):
    return n % 2 == 0

@pytest.mark.parametrize("input, expected", [
    (2, True),
    (3, False),
    (0, True),
    (-2, True),
    (-3, False),
])
def test_is_even(input, expected):
    assert is_even(input) == expected

这样写,一个测试函数就覆盖了 5 个场景。如果某个场景失败,pytest 会精确告诉你哪个参数组合出了问题。我在验证排序算法时经常用这个特性,一次跑几十组数据,效率极高。

4.3 测试用例编写规范

写测试不是随便写写就行的。我见过不少同学写的测试,要么只测了 happy path,要么断言写得模棱两可。这里我总结几条实战经验。

4.3.1 命名规范

  • 测试文件: test_<模块名>.py 或 <模块名>_test.py
  • 测试函数/方法: test_<被测试功能>_<场景>
  • 测试类(unittest): Test<被测试类>

举个例子,如果你要测试 sort.py 里的 quick_sort 函数,测试文件叫 test_sort.py,测试函数叫 test_quick_sort_normal、test_quick_sort_empty 等。这样一看名字就知道在测什么。

4.3.2 测试覆盖原则

一个合格的测试用例,至少要覆盖这几类场景:

  1. 正常情况: 最常见的输入,期望正常输出
  2. 边界情况: 比如空列表、0、最大值、最小值
  3. 异常情况: 非法输入,期望抛出异常或返回错误码
  4. 特殊值: 比如 None、空字符串、负数

我曾经在验证一个二分查找算法时,只测了正常数组,结果漏掉了数组为空的情况。上线后用户一查空数组,程序直接崩溃。嗯,从那以后我写测试必加边界条件。

4.3.3 断言规范

  • 一个测试只测一个逻辑点。 不要在一个测试函数里同时验证加法和除法。
  • 断言要精确。 不要用 assert result 这种模糊断言,要明确比较值。
  • 测试之间要独立。 不要依赖其他测试的执行结果或顺序。
注意: 测试用例之间不要共享可变状态。比如不要在测试 A 中修改了一个全局变量,然后在测试 B 中依赖这个修改。pytest 和 unittest 都不保证测试的执行顺序。

4.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

单元测试框架 unittest 继承 TestCase 类 self.assertEqual 等断言 setUp / tearDown 前后置 pytest 函数式 + assert @pytest.mark.parametrize fixture 依赖注入 编写规范 命名规范:test_xxx 覆盖:正常/边界/异常 断言精确,测试独立 核心原则:先写测试,再写代码 测试通过后,重构代码才有底气

这张图把 unittest、pytest 和编写规范三个部分串起来了。你可以看到,unittest 和 pytest 是两种不同的实现方式,但最终目标都是验证代码的正确性。而编写规范则是保证测试质量的关键。

4.5 实战建议

最后,给你几个实战中的小建议:

  • 从 pytest 开始学。 它更简单,上手快,适合初学者。等你熟悉了测试思维,再回头看 unittest 也不迟。
  • 每个算法函数至少写 3 个测试。 一个正常情况,一个边界情况,一个异常情况。这是最低标准。
  • 测试代码也是代码。 同样需要维护、重构。不要写一堆重复的测试代码,学会用参数化、fixture 等技巧。
  • 跑测试要养成习惯。 每次改完代码,顺手跑一下测试。我个人的习惯是:改代码前跑一次,改完后跑一次,确保没破坏原有功能。
我的经验: 刚开始写测试时,你可能会觉得「这功能太简单了,不用测」。但相信我,越简单的功能越容易出 bug。我曾经在一个「判断奇偶数」的函数里写错了取模符号,要不是测试帮我抓到,这个 bug 就上线了。

好了,这一章的内容就到这里。unittest 和 pytest 都是非常成熟的框架,选哪个看你个人喜好。但不管用哪个,写测试这个习惯一定要养成。下一章我们会聊更高级的测试技巧,比如 mock 和 fixture,到时候再见。

可可精品资料库