Unittest单元测试基础速记

理解单元测试

单元测试是对软件中最小的可测单元(函数、类方法)进行正确性验证的过程,是保障代码质量的第一道防线,能帮助开发者在项目前期发现 bug,降低修复成本。

类似简单的一个代码函数,对一个函数进行验证。

为什么要学单元测试

新手常有的疑问:我都 print 调试了,为什么要学这个?

想象这个场景:你写了个函数,手动把十几个场景挨个跑一遍,跑漏一个就上线背锅,下次改代码又得全部重跑一遍。单元测试就是把这些"手动跑一遍"固化成代码,一键回归,漏不掉、也忘不掉。

测试左移的理念

把测试提前到开发阶段,越早发现问题,修复成本越低。单元测试就是测试左移最直接的落地手段。

unittest 是什么

unittest 是 Python 自带的单元测试框架,其设计灵感源于 Java 的 JUnit 框架。无需安装三方库,具有稳定可靠、功能完备的特点。

unittest 能做什么

  • 自动化验证:代替手工调试,一键回归测试
  • 文档作用:测试用例本身就是需求和代码逻辑最佳的实例文档
  • 设计驱动:编写测试用例的过程有助于优化代码结构,使其更具可测性
  • 持续集成:CI/CD 流水线中不可或缺的一环

unittest 核心组件

  • TestCase
    • 是 unittest 里面最基本的单元,用于定义单个测试用例
    • 必须继承 unittest.TestCase 基类
    • 测试方法必须以 test_ 开头,如果不是这样 unittest 测试框架不会识别并运行测试用例
  • TestSuite
    • 测试套件,容器的作用,用来组织多个测试用例或测试类
  • TestRunner
    • 执行测试并输出结果的组件
  • TestFixture
    • 定义测试用例执行前的前置处理和后置清理工作

项目结构

新手常问 test 文件放哪、怎么命名。推荐这样组织:

text 复制代码
project/
├── src/
│   └── calc.py
└── tests/
    ├── __init__.py
    ├── test_calc.py
    └── test_data.json

约定:

  • 测试文件命名 test_*.py,unittest discover 默认按这个规则找
  • tests/ 目录下加一个 __init__.py,避免导入路径问题
  • 被测代码放 src/,测试代码放 tests/,分离清楚

写 demo

python 复制代码
# test_demo.py
import unittest


class TestAdd(unittest.TestCase):

    def test_add_1(self):
        self.assertEqual(1 + 1, 2)

    def test_add_2(self):
        self.assertEqual(2 + 2, 4)

    def test_add_3(self):
        self.assertEqual(3 + 3, 7)  # 故意失败


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

如果是命令行运行,需要写一个 main 函数(unittest.main())。

命令行运行:

bash 复制代码
python test_demo.py
# 或
python -m unittest test_demo.py

常用断言方法

新手必背清单,assertEqual 之外还有这些:

断言方法 含义 示例
assertEqual(a, b) a == b assertEqual(1 + 1, 2)
assertNotEqual(a, b) a != b assertNotEqual(1, 2)
assertTrue(x) x 为 True assertTrue(1 < 2)
assertFalse(x) x 为 False assertFalse(1 > 2)
assertIn(a, b) a 在 b 中 assertIn(1, [1, 2, 3])
assertNotIn(a, b) a 不在 b 中 assertNotIn(4, [1, 2, 3])
assertIsNone(x) x 是 None assertIsNone(None)
assertIsNotNone(x) x 不是 None assertIsNotNone(0)
assertIsInstance(a, cls) a 是 cls 实例 assertIsInstance("a", str)
assertAlmostEqual(a, b) 浮点数近似相等 assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
assertRaises(Exc) 抛出指定异常 见下方示例

assertRaises 两种写法:

python 复制代码
import unittest


class TestRaises(unittest.TestCase):

    def test_raises_context(self):
        # 写法一:上下文管理器
        with self.assertRaises(ZeroDivisionError):
            1 / 0

    def test_raises_callable(self):
        # 写法二:传入可调用对象
        self.assertRaises(ZeroDivisionError, lambda: 1 / 0)


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

浮点数别用 assertEqual,0.1 + 0.2 != 0.3,坑很多,用 assertAlmostEqual。

执行结果分析

  • .:测试通过
  • F:测试失败(Failure),断言失败
  • E:测试错误(Error),代码本身异常
  • S:测试跳过(Skip)

跑一遍上面的 demo,真实输出大概长这样:

text 复制代码
F..
======================================================================
FAIL: test_add_3 (test_demo.TestAdd.test_add_3)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_demo.py", line 13, in test_add_3
    self.assertEqual(3 + 3, 7)
AssertionError: 6 != 7

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

FAILED (failures=1)

逐行解读:

  • 第一行 F..:三个用例,第一个失败,后两个通过
  • FAIL: test_add_3 (test_demo.TestAdd.test_add_3):哪个类、哪个方法失败
  • Traceback:失败位置,精确到文件和行号
  • AssertionError: 6 != 7:断言实际值 vs 期望值,一看就知道差在哪
  • Ran 3 tests in 0.001s:跑了几个用例、耗时
  • FAILED (failures=1):总结论,failures 是断言失败数,errors 是代码异常数

UNITTEST 框架自动寻找 TestCase 子类并运行。

执行顺序是按方法名的 ASCII 码值大小排序,不是代码书写顺序。

测试用例独立性原则

新手最容易犯的错:用例 A 依赖用例 B 留下的数据。

必须遵守:

  • 每个用例能单独跑
  • 乱序跑结果不变
  • 重复跑结果不变
  • 用例之间不共享全局状态、不依赖执行顺序

反面例子:

python 复制代码
class TestBad(unittest.TestCase):
    data = []

    def test_01_add(self):
        self.data.append(1)  # 污染类属性

    def test_02_check(self):
        self.assertEqual(len(self.data), 0)  # 单跑 test_02 能过,一起跑就挂

数据准备放在 setUp 里,用完在 tearDown 清掉,这是最简单可靠的做法。

Unittest TestFixture

方法级

方法级前置处理:setUp

方法级后置处理:tearDown

python 复制代码
import unittest


class TestDemo(unittest.TestCase):

    def setUp(self):
        print("这是方法级的前置处理")

    def tearDown(self):
        print("这是方法级后置处理")

    def test_case_1(self):
        print("测试用例 1")

    def test_case_2(self):
        print("测试用例 2")
  • 每个测试方法执行前后都会各执行一次
  • 断言失败或用例报错,tearDown 依然会执行,适合做清理工作

类级

还有类级的前后置处理,需要加装饰器 @classmethod。

python 复制代码
import unittest


class TestDemo(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        print("类级前置:整个类执行前执行一次")

    @classmethod
    def tearDownClass(cls):
        print("类级后置:整个类执行后执行一次")

    def test_case_1(self):
        print("测试用例 1")

    def test_case_2(self):
        print("测试用例 2")
  • setUpClass、tearDownClass 整个类前后各执行一次
  • 必须加 @classmethod 装饰器,忘了直接报错

模块级

模块级(这个 py 文件)所有用例执行之前和所有用例执行之后执行一次。

注意:unittest 标准库不像 pytest 那样有 setup_module(),unittest 是在模块里定义 setUpModule() 和 tearDownModule() 函数(函数级,不是类方法)。

python 复制代码
import unittest


def setUpModule():
    print("模块级前置:整个模块执行前一次")


def tearDownModule():
    print("模块级后置:整个模块执行后一次")


class TestA(unittest.TestCase):

    def test_a_1(self):
        print("test_a_1")

    def test_a_2(self):
        print("test_a_2")


class TestB(unittest.TestCase):

    def test_b_1(self):
        print("test_b_1")


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

前后置等级对比

级别 前置 后置 作用范围 备注
方法级 setUp(self) tearDown(self) 每个测试方法 类内定义
类级 setUpClass(cls) tearDownClass(cls) 整个测试类 需加 @classmethod
模块级 setUpModule() tearDownModule() 整个模块 模块内函数定义

TestSuite

如果想多文件的测试用例全部一起运行呢?使用 TestSuite。

python 复制代码
import unittest
from test_module_a import TestA
from test_module_b import TestB


if __name__ == "__main__":
    # 1. 创建测试套件
    suite = unittest.TestSuite()

    # 2. 添加测试用例(可以是类,也可以是单个方法)
    suite.addTest(TestA("test_a_1"))
    suite.addTest(TestB("test_b_1"))

    # 也可以用 addTests 批量添加
    # suite.addTests([TestA("test_a_1"), TestB("test_b_1")])

    # 3. 创建执行器并执行
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

自动发现测试用例(多文件推荐用 discover):

python 复制代码
import unittest


if __name__ == "__main__":
    # 从当前目录开始查找 test 开头的 py 文件
    suite = unittest.defaultTestLoader.discover(start_dir=".", pattern="test_*.py")
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

verbosity 说明:

  • verbosity=0:静默,只输出结果摘要
  • verbosity=1:默认,点号显示
  • verbosity=2:详细,显示每个用例名称和执行结果

跳过测试

python 复制代码
import unittest


class TestDemo(unittest.TestCase):

    @unittest.skipIf(1 > 0, "条件成立,跳过")
    def test_skip_if(self):
        print("这条不会执行")

    @unittest.skip("无条件跳过")
    def test_skip(self):
        print("这条也不会执行")

    @unittest.skipUnless(False, "条件为 False 跳过")
    def test_skip_unless(self):
        print("这条也不会执行")

    def test_dynamic_skip(self):
        if True:
            self.skipTest("方法内动态跳过")
        print("这条也不会执行")

参数说明:

  • condition:判断条件,判断表达式,bool
  • reason:跳过原因,str 类型

常用装饰器:

  • @unittest.skip(reason):无条件跳过
  • @unittest.skipIf(condition, reason):条件为 True 时跳过
  • @unittest.skipUnless(condition, reason):条件为 False 时跳过
  • self.skipTest(reason):方法内动态跳过

数据驱动(ddt)

数据驱动的目的是将测试数据和用例逻辑进行分离,提高代码的可重用性。

安装:

bash 复制代码
pip install ddt

支持导入多种类型数据:有列表、json、yaml 等。

python 复制代码
import unittest
from ddt import ddt, data, unpack, file_data


@ddt  # 装饰器标记这个测试类使用数据驱动
class TestDDT(unittest.TestCase):

    @data(1, 2, 3)  # 单个参数
    def test_single(self, value):
        self.assertGreater(value, 0)

    @data((1, 2), (3, 4), (5, 6))  # 多个参数
    @unpack
    def test_multi(self, a, b):
        self.assertLess(a, b)

    @file_data("test_data.json")  # 从文件读取
    def test_from_file(self, **kwargs):
        print(kwargs)


if __name__ == "__main__":
    unittest.main()
  • @ddt:装饰器标记这个测试类使用的数据驱动
  • @data:直接传入数据
  • @unpack:拆解元组 / 列表,用于多参数
  • @file_data:从 json、yaml 文件读取数据

json 文件完整示例

test_data.json:

json 复制代码
[
  {"username": "admin", "password": "123456"},
  {"username": "guest", "password": "guest"}
]

配套代码:

python 复制代码
import unittest
from ddt import ddt, file_data


@ddt
class TestLogin(unittest.TestCase):

    @file_data("test_data.json")
    def test_login(self, username, password):
        self.assertTrue(len(username) > 0)
        self.assertTrue(len(password) > 0)


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

注意:file_data 加载 json 里的每个对象时,键名会直接变成方法参数名,所以 json 的 key 必须和函数参数对得上。

yaml 文件示例

需要额外装 PyYAML:

bash 复制代码
pip install pyyaml

test_data.yaml:

yaml 复制代码
- username: admin
  password: "123456"
- username: guest
  password: guest

用法和 json 一样:

python 复制代码
@file_data("test_data.yaml")
def test_login(self, username, password):
    self.assertTrue(len(username) > 0)

json 和 yaml 格式我还不太熟,需要额外熟悉一下。

并发测试(进阶)

进阶内容,初学可跳过。

unittest 本身不直接支持多进程并发,常见方案:

  • 用 concurrent.futures 手动并发跑多个 TestSuite
  • 用 pytest-xdist 搭配 pytest 运行:pytest -n 4

简单示例,用 concurrent.futures 并发跑不同测试类:

python 复制代码
import unittest
from concurrent.futures import ThreadPoolExecutor


class TestA(unittest.TestCase):
    def test_a_1(self):
        self.assertEqual(1 + 1, 2)


class TestB(unittest.TestCase):
    def test_b_1(self):
        self.assertEqual(2 + 2, 4)


def run_case(test_class):
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(test_class)
    runner = unittest.TextTestRunner(verbosity=2)
    return runner.run(suite)


if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=2) as executor:
        futures = [
            executor.submit(run_case, TestA),
            executor.submit(run_case, TestB),
        ]
        for f in futures:
            f.result()

注意点:

  • 并发执行时,不同用例之间不能共享全局状态,否则结果不稳定
  • 用 ThreadPoolExecutor 只是多线程并发,适合 I/O 密集;CPU 密集用 ProcessPoolExecutor
  • 更推荐用 pytest-xdist,命令一行 pytest -n 4 即可多进程并发

常见坑

  • 文件名不是 test_*.py,discover 找不到,用例一个都不跑
  • 测试类没继承 unittest.TestCase,用例静默不执行
  • 方法名不是 test_ 开头,框架直接忽略,不报错
  • setUpClass 忘了 @classmethod,直接报错
  • 浮点数用 assertEqual 比较,0.1 + 0.2 != 0.3,用 assertAlmostEqual
  • 用例之间共享全局变量,单跑能过,一起跑挂,排查半天
  • 忘了写 if __name__ == "__main__": unittest.main(),命令行跑没反应

注意点

  • 测试方法必须以 test_ 开头,否则框架不识别
  • 执行顺序按方法名 ASCII 码排序,不是代码书写顺序
  • setUp / tearDown 每个方法前后各执行一次;setUpClass / tearDownClass 整个类前后各一次,需加 @classmethod
  • 模块级用 setUpModule() / tearDownModule() 函数,不是类方法
  • 断言失败或用例报错,tearDown 仍会执行
  • ddt 需要额外安装:pip install ddt
  • yaml 数据驱动需要额外安装:pip install pyyaml
  • 并发执行要用 pytest-xdist 或 concurrent.futures,注意用例之间不能共享状态

unittest 掌握之后,可以去看 pytest。写法更简洁,不用继承类,直接用 assert,还有 fixture、参数化、插件生态,是目前更主流的 Python 测试框架。

相关推荐
ZLongTeng4 小时前
EIDE的launch.json文件
json
梦想不只是梦与想8 小时前
Python 中的 json 模块
python·json·对象转字符串
xie8810180210 小时前
正则里的点号不匹配换行:多行模式的开关
运维·服务器·测试工具
步行cgn11 小时前
JSON.stringify 详解:把 JavaScript 对象变成 JSON 字符串
开发语言·javascript·json
阿正的梦工坊12 小时前
深入理解 JSON-RPC 2.0:轻量级远程调用协议详解
rpc·json
腾讯数据架构师12 小时前
任务跑完怎么直接出图表?CubeStudio 任务可视化 ECharts 数据格式实操(CSV/JSON + metric.json,11 种图表一键渲染)
前端·json·echarts·数据可视化
救救孩子把13 小时前
结构化输出为何“格式正确、答案错误”?JSON Schema 的能力边界
android·java·json·agent·agent服务
霸道流氓气质14 小时前
智能表单生成器:结构化输出契约、JSON Schema 驱动渲染与 EAV/JSONB 混合存储的 Vue3 + Spring AI 生产级实战
人工智能·spring·json
SEO_juper15 小时前
Bash 交叉验证 robots.txt 和 sitemap:两个文件自相矛盾,页面永远收录不了
开发语言·python·node.js·json·bash