unittest 标准库入门
unittest 是 Python 标准库自带 的单元测试框架(无需 pip 安装),提供测试固件、测试用例、测试运行器、测试报告四大核心组件。
教学顺序:先学 unittest 理解测试框架的"固件 → 用例 → 运行 → 报告"完整链路,再学 pytest 看它如何把这些环节变得更简洁灵活。
① 四大核心组件
测试固件 Fixture
setUp() 每个用例前执行(准备数据),tearDown() 每个用例后执行(清理资源)
测试用例 TestCase
继承 unittest.TestCase,方法名以 test_ 开头,用 assert* 系列断言
测试运行器 Runner
unittest.TextTestRunner 负责收集并执行所有用例,输出结果到终端
测试报告 Report
终端输出 ./F/E 标记,统计 Run / Failures / Errors / OK
② 测试固件 Fixture 详解
固件(Fixture)就是"为每个测试准备/清理环境"的钩子方法。unittest 提供三级固件:模块级别(整个 .py 文件只跑 1 次)、类级别(每个 TestCase 类只跑 1 次)、方法级别(每个 test_ 方法都跑)。
setUpModule
→
setUpClass
→
setUp
→
test_xxx
→
tearDown
→
tearDownClass
→
tearDownModule
| 级别 | 方法 | 什么时候调用 | 典型用途 |
方法级别 (Method) | setUp(self) | 每个 test_ 方法之前 | 准备数据 / 刷新页面 / 新建连接 |
tearDown(self) | 每个 test_ 方法之后 | 清理数据 / 关闭页面 / 断开连接 |
类级别 (Class) | setUpClass(cls) | 每个 TestCase 类所有方法之前,只执行 1 次 | 重量级初始化:连一次浏览器 / 加载大 fixture |
tearDownClass(cls) | 每个 TestCase 类所有方法之后,只执行 1 次 | 重量级清理:关闭浏览器 / 释放资源 |
模块级别 (Module) | setUpModule() | 整个 .py 文件所有类之前,只执行 1 次 | 全局初始化:读配置 / 连数据库 / 启动 Web 服务 |
tearDownModule() | 整个文件所有类之后,只执行 1 次 | 全局清理:关数据库 / 停服务 |
import unittest
# ── 模块级别固件(整个 test_xxx.py 只跑 1 次,不需要 @classmethod!) ──
def setUpModule():
print("🟡 setUpModule — 整个 .py 文件最开始")
# 全局配置读取 / 数据库连接 / 启动 mock server
def tearDownModule():
print("🟡 tearDownModule — 整个 .py 文件最后")
# 关闭数据库 / 停服务
class TestLogin(unittest.TestCase):
# ── 类级别固件(每个 TestCase 类只跑 1 次) ──
@classmethod
def setUpClass(cls):
print(f" 🟢 setUpClass — 类级别只跑 1 次")
cls.driver = webdriver.Chrome()
@classmethod
def tearDownClass(cls):
print(f" 🔴 tearDownClass — 类级别只跑 1 次")
cls.driver.quit()
# ── 方法级别固件(每个 test_ 方法都跑) ──
def setUp(self):
print(f" ⏱ setUp — 每个用例前")
self.driver.refresh()
self.driver.get("/login")
def tearDown(self):
print(f" ⏹ tearDown — 每个用例后")
self.driver.delete_all_cookies()
# ── 测试用例 ──
def test_login_admin(self):
print(" ▶ test_login_admin")
pass
def test_login_user(self):
print(" ▶ test_login_user")
pass
⚠️ 关键细节:
① 模块级别 setUpModule/tearDownModule 是模块级函数(不在 class 里),不需要 @classmethod 装饰器;
② 类级别 setUpClass/tearDownClass 是类方法,必须加 @classmethod,参数是 cls 不是 self;
③ 三级固件共享数据时要小心:模块级别用全局变量、类级别用 cls.driver、方法级别用 self.driver——作用域不同容易搞混。
📌 pytest 对应关系:pytest 没有 setUp/tearDown 方法族,用 一个 fixture 函数 + scope 参数 实现三级固件:@pytest.fixture(scope="session") ≈ setUpModule,scope="class" ≈ setUpClass,scope="function" ≈ setUp。更简洁。
📊 unittest 3 级 ↔ pytest 4 级 对照
| 生命周期 | unittest | pytest scope | pytest 写法 |
整个测试会话 (多个 .py 文件) | ❌ 没有 | session | @fixture(scope="session") |
| 单个 .py 文件 | setUpModule() / tearDownModule() | module | @fixture(scope="module") |
| 单个 TestCase 类 | @classmethod setUpClass(cls) | class | @fixture(scope="class") |
| 每个 test_ 方法 | setUp(self) / tearDown(self) | function (默认值) | @fixture |
💡 关键差异:pytest 多了一个 session 级别——整个 pytest tests/ 跑起来,多个 .py 文件共享一次初始化。unittest 做不到跨文件的 session 级别的固件,要自己写 orchestration 脚本手动调 setUpModule。
③ 测试用例 TestCase 详解
🧪 什么是 TestCase?一个 unittest.TestCase 子类就是一组相关测试的集合。类里每个以 test_ 开头的方法,都是一条独立的测试用例。unittest 会自动发现并运行它们。
📐 TestCase 的最小骨架(Web 测试场景)
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
# ① 必须继承 unittest.TestCase
class TestLoginPage(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Chrome()
self.driver.get("http://localhost:5000/login")
# ② 每个测试方法必须以 test_ 开头
def test_login_success(self):
# 输入用户名密码 → 点登录 → 跳到首页
self.driver.find_element(By.ID, "username").send_keys("admin")
self.driver.find_element(By.ID, "password").send_keys("123456")
self.driver.find_element(By.ID, "login-btn").click()
# ③ 用 assert* 方法断言
self.assertIn("首页", self.driver.title)
def test_login_empty_password(self):
self.driver.find_element(By.ID, "username").send_keys("admin")
self.driver.find_element(By.ID, "login-btn").click()
error_msg = self.driver.find_element(By.ID, "error").text
self.assertEqual(error_msg, "密码不能为空")
def not_a_test_method(self): # 没有 test_ 前缀 → 不会被运行!
pass
def tearDown(self):
self.driver.quit()
if __name__ == "__main__":
unittest.main(verbosity=2)
unittest.TestCase
→ 继承
class TestXxx
→ 包含
setUp()
+
test_xxx()
+
tearDown()
🏷️ 测试用例命名规范
方法名要让别人看名字就知道测什么。推荐模式:test_被测功能_前置条件_预期结果
| ❌ 坏命名 | ✅ 好命名 | 为什么好 |
test1 | test_login_admin_success | 一眼看出测"管理员登录成功" |
test_ok | test_login_empty_password_should_fail | 前置条件 + 预期结果都写清楚 |
test_button | test_login_button_click_navigates_to_home | 动作 + 导航目标明确 |
test_2 | test_divide_by_zero_raises_error | 边界场景 + 异常类型明确 |
calc | test_add_two_positive_numbers | 没有 test_ 前缀 → 根本不会被运行 |
💡 行业常用命名模式:
• test_{功能}_{场景}_{预期}(unittest / pytest 通用)
• test_when_{条件}_then_{结果}(BDD 风格,pytest 更常见)
• 全小写 + 下划线分隔,别用驼峰:test_login_ok ✅ vs testLoginOk ❌
🔄 调用顺序 & 独立性
📌 核心原则:每条用例独立运行。unittest 在每个 test_ 方法前后都会调 setUp/tearDown,目的就是让你可以单独跑某一条用例,不需要先跑其他用例。
class TestLogin(unittest.TestCase):
def setUp(self):
print(" ⏱ setUp")
def test_b(self): # 字母 b 开头
print(" ▶ test_b")
def test_a(self): # 字母 a 开头
print(" ▶ test_a")
def test_c(self): # 字母 c 开头
print(" ▶ test_c")
def tearDown(self):
print(" ⏹ tearDown")
运行输出:
$ python test_order.py -v
test_a ... ⏱ → ▶ test_a → ⏹ ok
test_b ... ⏱ → ▶ test_b → ⏹ ok
test_c ... ⏱ → ▶ test_c → ⏹ ok
Ran 3 tests in 0.001s
OK
- ✅ 执行顺序 = 字母序:
test_a → test_b → test_c(不是代码里写的顺序!)
- ✅ 独立运行:每个 test_ 都有自己的 setUp → test → tearDown 周期
- ✅ 单独跑某一条:
python -m unittest test_order.TestLogin.test_b -v 只跑 test_b
- ❌ 不要有顺序依赖:
test_login 成功了才能跑 test_logout——这说明设计有问题,应该拆成独立的登录测试 + 登出测试(各自 setUp 里先登录)
📋 TestCase 三大规则
| 规则 | 正确示例 | 错误示例 | 为什么 |
| 必须继承 TestCase | class TestLogin(unittest.TestCase) | class TestLogin: | 不继承 → 不是测试类,runner 不认识 |
方法必须 test_ 开头 | def test_login_ok(self): | def login_ok(self): | 没有 test_ 前缀 → 被当成普通方法跳过 |
用 assert* 断言 | self.assertEqual(a, b) | assert a == b | 内置 assert 失败时 unittest 可能不会正确计为 FAILURE |
⚠️ setUp/tearDown 陷阱:如果你重写了父类 setUp 但忘了调 super().setUp(),那么父类 TestCase 的初始化逻辑不会执行,可能导致断言方法行为异常。unittest 官方建议里不需要调 super(因为 TestCase 基类的 setUp/tearDown 是空方法),但如果你的 TestCase 有多层继承链(比如自定义 BaseTest),就一定要调。
def setUp(self):
super().setUp() # ← 多层继承时必加
self.driver = webdriver.Chrome()
📌 pytest 对应关系:pytest 不需要继承 TestCase,普通函数 def test_xxx(): 就是一条用例。而且直接写 assert a == b 就行——pytest 会自动做更友好的 diff 输出。少了很多"必须遵守的规则"。
④ 完整代码示例(计算器)
# calc.py —— 被测模块
class Calculator:
def add(self, a, b): return a + b
def subtract(self, a, b): return a - b
def multiply(self, a, b): return a * b
def divide(self, a, b):
if b == 0:
raise ValueError("除数不能为 0")
return a / b
# test_calc.py —— unittest 测试
import unittest
from calc import Calculator
class TestCalculator(unittest.TestCase):
# ① setUp —— 测试固件:每个用例运行前自动执行
def setUp(self):
# 准备一个干净的计算器实例
self.calc = Calculator()
self.calls = 0 # 可选:每个用例独立的计数器
# ② tearDown —— 测试固件:每个用例结束后自动执行
def tearDown(self):
self.calc = None # 释放资源(实际场景:关闭文件/数据库连接)
# ③ 测试用例:方法名必须以 test_ 开头
def test_add(self):
self.assertEqual(self.calc.add(2, 3), 5)
self.assertEqual(self.calc.add(-1, 1), 0)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
def test_multiply(self):
self.assertAlmostEqual(self.calc.multiply(0.1, 0.2), 0.02)
# ④ 测试运行器:执行入口
if __name__ == "__main__":
# TextTestRunner 负责收集用例并输出报告
unittest.main(verbosity=2) # verbosity=2 显示每个测试用例名
⑤ 如何调用测试用例
💡 unittest.main() 背后的调用链
你写的 unittest.main() 其实做了 3 件事:
TestLoader
→ 自动发现
TestSuite
→ 收集用例
TextTestRunner
→ 执行并输出
测试结果
📋 四种调用方式
① 方式 1:直接运行(最简单)
# 文件末尾写这两行:
if __name__ == "__main__":
unittest.main(verbosity=2)
# 然后命令行:
python test_calc.py
✅ 推荐教学用。verbosity=2 显示每个测试用例名,=1 只显示 .F.E
② 方式 2:命令行指定模块
# 跑整个文件
python -m unittest test_calc -v
# 跑某个 TestCase 类
python -m unittest test_calc.TestCalculator -v
# 跑单个方法
python -m unittest test_calc.TestCalculator.test_add -v
✅ CI/CD 场景常用。-v 详细模式。
③ 方式 3:discover 自动发现
# 扫描当前目录下所有 test_*.py
python -m unittest discover -s . -p "test_*.py" -v
# -s 起始目录 / -p 文件模式
✅ 项目里有多个测试文件时用。扫描规则:文件名 test_*.py,方法名 test_*。
④ 方式 4:TestLoader + TestSuite + TextTestRunner(高级)
import unittest
# 第一步:TestLoader 自动发现并加载
loader = unittest.TestLoader()
suite = loader.discover(start_dir="tests", pattern="test_*.py")
# 第二步:手动组合 TestSuite(灵活控制顺序/分组)
suite = unittest.TestSuite()
suite.addTest(test_calc.TestCalculator("test_add"))
suite.addTest(test_login.TestLoginPage("test_login_success"))
# 第三步:TextTestRunner 执行并输出结果
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# 第四步:result 里能拿到统计数据
print(f"成功 {result.testsRun - len(result.failures) - len(result.errors)}/{result.testsRun}")
✅ 企业级场景用。可以自定义输出格式、把结果写进 HTML 报告、集成到 CI 系统。unittest.main() 底层其实就是这套。
📌 pytest 对应关系:pytest 直接 pytest tests/ -v 就行——不需要你关心 TestLoader/TestSuite/TestRunner 的分工。它自动发现、收集、执行、生成报告。unittest 是手动模式,pytest 是全自动模式。
⑥ 测试报告示例(终端输出)
$ python test_calc.py
test_add (test_calc.TestCalculator.test_add) ... ok
test_divide_by_zero (test_calc.TestCalculator.test_divide_by_zero) ... ok
test_multiply (test_calc.TestCalculator.test_multiply) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
$ python test_calc.py # 假设某个断言失败
test_add (test_calc.TestCalculator.test_add) ... FAIL
test_divide_by_zero ... ok
test_multiply ... ok
----------------------------------------------------------------------
FAIL: test_add (test_calc.TestCalculator.test_add)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_calc.py", line 22, in test_add
self.assertEqual(self.calc.add(2, 3), 6) # 故意写错期望值
AssertionError: 5 != 6
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
⑦ assert* 断言方法速查表
| 断言方法 | 作用 | 等价于 |
assertEqual(a, b) | a == b | assert a == b |
assertNotEqual(a, b) | a != b | assert a != b |
assertTrue(x) | bool(x) 为 True | assert x |
assertFalse(x) | bool(x) 为 False | assert not x |
assertIs(a, b) | a 是 b(同一对象) | assert a is b |
assertIsNone(x) | x 是 None | assert x is None |
assertIn(a, b) | a 在 b 里面 | assert a in b |
assertIsInstance(a, b) | a 是 b 的实例 | assert isinstance(a, b) |
assertAlmostEqual(a, b, places=2) | 浮点数近似相等 | round(a-b, 2) == 0 |
assertRaises(异常类) | with 块内抛出异常 | pytest 的 pytest.raises |
assertRegex(text, pattern) | 正则匹配 | assert re.search(pattern, text) |
⑧ 跳过用例 skip / skipIf / skipUnless
某些环境下特定用例不需要跑(如 Mac 跑不了 Windows 专属测试),用 @unittest.skip* 装饰器标记,报告里显示为 S(SKIP):
import sys
import unittest
# 无条件跳过
@unittest.skip("此功能暂未实现")
def test_not_ready(self):
pass
# 条件跳过:Windows 上才跑,其他平台跳过
@unittest.skipUnless(sys.platform == "win32", "仅 Windows 支持")
def test_windows_feature(self):
pass
# 反向条件:如果没有 selenium 就跳过
@unittest.skipIf(not HAS_SELENIUM, "selenium 未安装")
def test_ui(self):
pass
# 预期失败:明确知道这条用例会失败,不算红色
@unittest.expectedFailure
def test_known_bug(self):
self.assertEqual(1, 2) # 预期失败,报告显示 x
⑨ subTest —— 一个方法跑多组数据
不想为每组数据写一个测试方法?用 self.subTest() 循环执行,每组数据独立报告:
class TestCalc(unittest.TestCase):
def test_add_all(self):
# 一组输入数据:(a, b, expected)
cases = [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
("a", 1, TypeError), # 故意写错,看 subTest 报告
]
for a, b, expected in cases:
with self.subTest(a=a, b=b):
self.assertEqual(a + b, expected)
# 报告中:哪个 (a,b) 组合失败一目了然,不影响其他组结果
⑩ discover 自动发现规则
# 目录结构示例
project/
├── tests/
│ ├── __init__.py ← 让 tests 成为 package
│ ├── test_calc.py ← 被 discover 扫描(test_ 前缀)
│ ├── test_login.py ← 被扫描
│ └── helper.py ← 不扫描(非 test_ 开头)
python -m unittest discover -s tests -p "test_*.py" -v
↑ ↑ ↑
│ │ └─ 文件名匹配模式(默认 "test*.py")
│ └──────────── 扫描起始目录
└────────────────────── discover 子命令
⑪ unittest vs pytest 对比
| 对比项 | unittest | pytest |
| 安装 | ✅ 标准库自带 | pip install pytest |
| 写用例 | 必须继承 TestCase 类 | 普通函数即可 |
| 断言 | self.assertEqual 等 21 种 | 原生 assert + 丰富失败信息 |
| 固件 | setUp/tearDown + setUpClass | 灵活的 fixture(scope 控制) |
| 参数化 | subTest 循环(手动) | @pytest.mark.parametrize 装饰器 |
| 跳过用例 | @skip / @skipIf / @skipUnless | @pytest.mark.skip / skipif |
| 运行命令 | python -m unittest discover | pytest(自动发现) |
| 插件生态 | 几乎无 | 3000+ 插件(cov / html / xdist / mock 等) |
| HTML 报告 | 需 xmlrunner 第三方包 | pytest-html 一键生成 |
→ 下一节:unittest 是"正确但繁琐"的标准实现,学完它再看 pytest 就能瞬间看懂 pytest "简化了哪些环节、替你做了哪些事"。