第1章 unittest基础架构与核心概念
1.1 unittest框架架构总览
unittest是Python标准库中的单元测试框架,其设计灵感来源于Java的JUnit。理解其架构是掌握unittest的第一步。
执行流程
unittest核心架构
TestCase
测试用例基类
TestSuite
测试套件
TestLoader
测试加载器
TestRunner
测试运行器
TestResult
测试结果
Fixture
测试夹具
加载测试
组织套件
执行测试
收集结果
生成报告
1.2 核心组件详解
1.2.1 TestCase - 测试用例基类
TestCase是unittest中最重要的类,所有测试类都必须继承它。
python
import unittest
class TestStringMethods(unittest.TestCase):
"""
TestCase基类详解
继承自unittest.TestCase的类会自动被识别为测试类
以test_开头的方法会被识别为测试方法
"""
def test_upper(self):
"""测试字符串upper方法"""
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
"""测试字符串isupper方法"""
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
"""测试字符串split方法"""
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# 测试分隔符不存在的情况
with self.assertRaises(TypeError):
s.split(2) # split需要字符串参数
if __name__ == '__main__':
unittest.main()
1.2.2 TestSuite - 测试套件
TestSuite用于组织和组合多个测试用例或测试套件。
python
import unittest
class TestMathOperations(unittest.TestCase):
def test_addition(self):
self.assertEqual(1 + 1, 2)
def test_subtraction(self):
self.assertEqual(3 - 1, 2)
class TestStringOperations(unittest.TestCase):
def test_concatenation(self):
self.assertEqual('a' + 'b', 'ab')
# 手动构建测试套件
def create_test_suite():
"""
手动创建测试套件
适用于需要精确控制测试执行顺序的场景
"""
suite = unittest.TestSuite()
# 添加单个测试方法
suite.addTest(TestMathOperations('test_addition'))
# 添加整个测试类
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestStringOperations))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity=2)
runner.run(create_test_suite())
1.2.3 TestLoader - 测试加载器
TestLoader负责从各种来源加载测试。
python
import unittest
# TestLoader主要方法说明
loader = unittest.TestLoader()
# 1. loadTestsFromTestCase(testCaseClass) - 从测试类加载
# 2. loadTestsFromModule(module, pattern=None) - 从模块加载
# 3. loadTestsFromName(name, module=None) - 从名称加载
# 4. loadTestsFromNames(names, module=None) - 从多个名称加载
# 5. discover(start_dir, pattern='test*.py', top_level_dir=None) - 自动发现
class TestLoaderDemo(unittest.TestCase):
def test_demo(self):
pass
if __name__ == '__main__':
# 演示不同加载方式
print("TestLoader方法演示:")
print(f"1. loadTestsFromTestCase: {loader.loadTestsFromTestCase(TestLoaderDemo)}")
1.2.4 TestRunner - 测试运行器
TestRunner负责执行测试并输出结果。
python
import unittest
import sys
class SimpleTest(unittest.TestCase):
def test_pass(self):
self.assertTrue(True)
def test_fail(self):
self.assertTrue(False, "这是一个失败的测试")
# 自定义TestRunner
class CustomTestRunner(unittest.TextTestRunner):
"""
自定义测试运行器
可以自定义输出格式和报告方式
"""
def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1,
failfast=False, buffer=False, resultclass=None):
super().__init__(stream, descriptions, verbosity, failfast, buffer, resultclass)
def run(self, test):
print("=" * 60)
print("开始执行自定义测试运行器...")
print("=" * 60)
result = super().run(test)
print("=" * 60)
print(f"测试执行完成!运行: {result.testsRun}, 失败: {len(result.failures)}")
print("=" * 60)
return result
if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(SimpleTest)
runner = CustomTestRunner(verbosity=2)
runner.run(suite)
1.2.5 TestResult - 测试结果
TestResult存储测试执行的结果信息。
python
import unittest
class TestResultDemo(unittest.TestCase):
def test_example(self):
self.assertEqual(1, 1)
if __name__ == '__main__':
# 创建测试结果对象
result = unittest.TestResult()
# 运行测试
suite = unittest.TestLoader().loadTestsFromTestCase(TestResultDemo)
suite.run(result)
# 查看结果属性
print(f"\n测试结果详情:")
print(f"testsRun: {result.testsRun}") # 运行的测试数量
print(f"failures: {len(result.failures)}") # 失败数量
print(f"errors: {len(result.errors)}") # 错误数量
print(f"skipped: {len(result.skipped)}") # 跳过数量
print(f"expectedFailures: {len(result.expectedFailures)}") # 预期失败
print(f"unexpectedSuccesses: {len(result.unexpectedSuccesses)}") # 意外成功
1.3 测试执行流程详解
TestResult TestCase TestRunner TestSuite TestLoader 用户代码 TestResult TestCase TestRunner TestSuite TestLoader 用户代码 loop [每个测试用例] 请求加载测试 扫描测试类/方法 创建测试套件 组织测试用例 运行测试套件 创建结果对象 执行setUp() 执行test_*() 执行tearDown() 记录结果 返回完整结果
1.4 第一个完整的unittest示例
python
import unittest
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):
"""除法运算,除数为0时抛出异常"""
if b == 0:
raise ValueError("除数不能为零")
return a / b
class TestCalculator(unittest.TestCase):
"""
Calculator类的完整测试用例
展示了unittest的基本用法
"""
@classmethod
def setUpClass(cls):
"""类级别:所有测试前执行一次"""
print("\n[setUpClass] 初始化测试环境...")
cls.calculator = Calculator()
@classmethod
def tearDownClass(cls):
"""类级别:所有测试后执行一次"""
print("[tearDownClass] 清理测试环境...")
def setUp(self):
"""方法级别:每个测试前执行"""
print(f" [setUp] 准备测试: {self._testMethodName}")
def tearDown(self):
"""方法级别:每个测试后执行"""
print(f" [tearDown] 清理测试: {self._testMethodName}")
def test_add(self):
"""测试加法运算"""
result = self.calculator.add(3, 5)
self.assertEqual(result, 8)
# 测试负数
result = self.calculator.add(-3, -5)
self.assertEqual(result, -8)
# 测试零
result = self.calculator.add(0, 5)
self.assertEqual(result, 5)
def test_subtract(self):
"""测试减法运算"""
result = self.calculator.subtract(10, 3)
self.assertEqual(result, 7)
def test_multiply(self):
"""测试乘法运算"""
result = self.calculator.multiply(4, 3)
self.assertEqual(result, 12)
def test_divide(self):
"""测试除法运算"""
result = self.calculator.divide(10, 2)
self.assertEqual(result, 5.0)
def test_divide_by_zero(self):
"""测试除零异常"""
with self.assertRaises(ValueError) as context:
self.calculator.divide(10, 0)
self.assertEqual(str(context.exception), "除数不能为零")
if __name__ == '__main__':
# verbosity参数控制输出详细程度
# 0: 静默模式, 1: 默认, 2: 详细模式
unittest.main(verbosity=2)
第2章 断言方法全解与参数详解
2.1 断言方法分类总览
unittest断言
相等性断言
assertEqual
assertNotEqual
assertAlmostEqual
assertNotAlmostEqual
assertCountEqual
assertListEqual
assertTupleEqual
assertSetEqual
assertDictEqual
布尔断言
assertTrue
assertFalse
assertIs
assertIsNot
assertIsNone
assertIsNotNone
比较断言
assertGreater
assertGreaterEqual
assertLess
assertLessEqual
异常断言
assertRaises
assertRaisesRegex
assertWarns
assertWarnsRegex
其他断言
assertIn
assertNotIn
assertIsInstance
assertNotIsInstance
assertRegex
assertNotRegex
2.2 相等性断言详解
python
import unittest
class TestEqualityAssertions(unittest.TestCase):
"""
相等性断言方法详解
用于验证两个值是否相等或不相等
"""
def test_assertEqual(self):
"""
assertEqual(first, second, msg=None)
验证first == second
如果失败,显示详细的差异信息
"""
# 基本用法
self.assertEqual(1 + 1, 2)
self.assertEqual("hello", "hello")
self.assertEqual([1, 2, 3], [1, 2, 3])
# 带自定义消息
self.assertEqual(
len([1, 2, 3]),
3,
"列表长度应该为3"
)
def test_assertNotEqual(self):
"""
assertNotEqual(first, second, msg=None)
验证first != second
"""
self.assertNotEqual(1, 2)
self.assertNotEqual("hello", "world")
self.assertNotEqual([1, 2], [1, 2, 3])
def test_assertAlmostEqual(self):
"""
assertAlmostEqual(first, second, places=7, msg=None, delta=None)
验证两个浮点数近似相等
参数:
places: 小数点后比较的位数,默认7位
delta: 最大允许差值,与places互斥
"""
# 使用places参数
self.assertAlmostEqual(3.14159, 3.14158, places=4)
# 3.1415 == 3.1415 (前4位相同)
# 使用delta参数
self.assertAlmostEqual(3.14159, 3.14, delta=0.01)
# 差值0.00159 < 0.01,通过
# 浮点数运算精度问题
result = 0.1 + 0.2
self.assertAlmostEqual(result, 0.3, places=10)
def test_assertNotAlmostEqual(self):
"""
assertNotAlmostEqual(first, second, places=7, msg=None, delta=None)
验证两个浮点数不近似相等
"""
self.assertNotAlmostEqual(3.14, 3.14159, places=4)
def test_assertCountEqual(self):
"""
assertCountEqual(first, second, msg=None)
验证两个序列包含相同的元素,不考虑顺序
类似于比较两个多重集合(multiset)
"""
self.assertCountEqual([1, 2, 3], [3, 2, 1])
self.assertCountEqual(['a', 'b', 'a'], ['a', 'a', 'b'])
# 失败示例:元素数量不同
# self.assertCountEqual([1, 2, 2], [1, 2]) # 失败!
def test_assertListEqual(self):
"""
assertListEqual(list1, list2, msg=None)
验证两个列表相等,考虑顺序
失败时显示列表差异的详细信息
"""
self.assertListEqual([1, 2, 3], [1, 2, 3])
# 与assertEqual的区别:assertListEqual专门用于列表,错误信息更友好
def test_assertTupleEqual(self):
"""
assertTupleEqual(tuple1, tuple2, msg=None)
验证两个元组相等
"""
self.assertTupleEqual((1, 2, 3), (1, 2, 3))
def test_assertSetEqual(self):
"""
assertSetEqual(set1, set2, msg=None)
验证两个集合相等
"""
self.assertSetEqual({1, 2, 3}, {3, 2, 1})
def test_assertDictEqual(self):
"""
assertDictEqual(d1, d2, msg=None)
验证两个字典相等
失败时显示字典差异的详细信息
"""
dict1 = {'a': 1, 'b': 2, 'c': {'nested': True}}
dict2 = {'a': 1, 'b': 2, 'c': {'nested': True}}
self.assertDictEqual(dict1, dict2)
if __name__ == '__main__':
unittest.main(verbosity=2)
2.3 布尔与身份断言详解
python
import unittest
class TestBooleanAssertions(unittest.TestCase):
"""
布尔断言和身份断言详解
"""
def test_assertTrue(self):
"""
assertTrue(expr, msg=None)
验证表达式为真(布尔值为True)
注意:expr可以是任何对象,非零、非空、非None都为真
"""
self.assertTrue(True)
self.assertTrue(1)
self.assertTrue("hello")
self.assertTrue([1, 2, 3])
self.assertTrue({'key': 'value'})
# 带消息
value = 42
self.assertTrue(value > 0, f"值应该大于0,实际为{value}")
def test_assertFalse(self):
"""
assertFalse(expr, msg=None)
验证表达式为假(布尔值为False)
"""
self.assertFalse(False)
self.assertFalse(0)
self.assertFalse("")
self.assertFalse([])
self.assertFalse({})
self.assertFalse(None)
def test_assertIs(self):
"""
assertIs(expr1, expr2, msg=None)
验证expr1和expr2是同一个对象(使用is运算符)
检查身份相等,而非值相等
"""
a = [1, 2, 3]
b = a # b引用同一个对象
self.assertIs(a, b)
# 小整数缓存
x = 5
y = 5
self.assertIs(x, y) # Python缓存小整数
# 字符串驻留
s1 = "hello"
s2 = "hello"
self.assertIs(s1, s2) # 字符串可能被驻留
def test_assertIsNot(self):
"""
assertIsNot(expr1, expr2, msg=None)
验证expr1和expr2不是同一个对象
"""
a = [1, 2, 3]
b = [1, 2, 3] # 新对象
self.assertIsNot(a, b)
self.assertEqual(a, b) # 但值相等
def test_assertIsNone(self):
"""
assertIsNone(expr, msg=None)
验证expr是None
"""
result = None
self.assertIsNone(result)
def test_assertIsNotNone(self):
"""
assertIsNotNone(expr, msg=None)
验证expr不是None
"""
result = "something"
self.assertIsNotNone(result)
if __name__ == '__main__':
unittest.main(verbosity=2)
2.4 比较断言详解
python
import unittest
class TestComparisonAssertions(unittest.TestCase):
"""
比较断言方法详解
用于数值大小比较
"""
def test_assertGreater(self):
"""
assertGreater(a, b, msg=None)
验证a > b
"""
self.assertGreater(10, 5)
self.assertGreater(3.14, 3.0)
self.assertGreater("b", "a") # 字符串按字典序比较
def test_assertGreaterEqual(self):
"""
assertGreaterEqual(a, b, msg=None)
验证a >= b
"""
self.assertGreaterEqual(10, 10)
self.assertGreaterEqual(10, 5)
def test_assertLess(self):
"""
assertLess(a, b, msg=None)
验证a < b
"""
self.assertLess(5, 10)
self.assertLess(3.0, 3.14)
def test_assertLessEqual(self):
"""
assertLessEqual(a, b, msg=None)
验证a <= b
"""
self.assertLessEqual(5, 5)
self.assertLessEqual(3, 5)
def test_comparison_in_practice(self):
"""
比较断言在实际中的应用
"""
# 验证年龄限制
age = 25
self.assertGreaterEqual(age, 18, "必须年满18岁")
# 验证列表长度
items = [1, 2, 3, 4, 5]
self.assertGreaterEqual(len(items), 3, "至少需要3个元素")
# 验证性能阈值
execution_time = 0.5 # 秒
self.assertLess(execution_time, 1.0, "执行时间应小于1秒")
if __name__ == '__main__':
unittest.main(verbosity=2)
2.5 异常断言详解
python
import unittest
import warnings
class TestExceptionAssertions(unittest.TestCase):
"""
异常断言方法详解
用于验证代码是否抛出预期的异常
"""
def test_assertRaises_basic(self):
"""
assertRaises(exc_class, callable, *args, **kwargs)
assertRaises(exc_class) [作为上下文管理器]
验证callable调用时抛出指定异常
"""
# 方式1:作为上下文管理器(推荐)
with self.assertRaises(ZeroDivisionError):
result = 1 / 0
# 方式2:传入可调用对象
self.assertRaises(ValueError, int, "not a number")
def test_assertRaises_with_context(self):
"""
使用上下文管理器捕获异常对象
可以进一步验证异常详情
"""
with self.assertRaises(ValueError) as context:
raise ValueError("自定义错误消息")
# 验证异常消息
self.assertEqual(str(context.exception), "自定义错误消息")
# 访问异常类型
self.assertIsInstance(context.exception, ValueError)
def test_assertRaisesRegex(self):
"""
assertRaisesRegex(exc_class, regex, callable, *args, **kwargs)
assertRaisesRegex(exc_class, regex) [作为上下文管理器]
验证抛出异常且消息匹配正则表达式
参数:
exc_class: 期望的异常类型
regex: 正则表达式字符串或Pattern对象
"""
# 验证异常消息包含特定文本
with self.assertRaisesRegex(ValueError, "invalid"):
raise ValueError("invalid input provided")
# 使用正则表达式模式
with self.assertRaisesRegex(ValueError, r"\d+"):
raise ValueError("Error code: 404")
def test_assertWarns(self):
"""
assertWarns(warn_class, callable, *args, **kwargs)
assertWarns(warn_class) [作为上下文管理器]
验证代码产生指定的警告
"""
import warnings
# 方式1:上下文管理器
with self.assertWarns(DeprecationWarning):
warnings.warn("此方法已弃用", DeprecationWarning)
# 方式2:可调用对象
def trigger_warning():
warnings.warn("注意!", UserWarning)
self.assertWarns(UserWarning, trigger_warning)
def test_assertWarns_with_context(self):
"""
捕获警告对象进行详细验证
"""
with self.assertWarns(UserWarning) as warning_context:
warnings.warn("这是一条警告", UserWarning)
# 验证警告消息
self.assertIn("警告", str(warning_context.warning))
def test_assertWarnsRegex(self):
"""
assertWarnsRegex(warn_class, regex, callable, *args, **kwargs)
assertWarnsRegex(warn_class, regex) [作为上下文管理器]
验证产生警告且消息匹配正则表达式
"""
with self.assertWarnsRegex(DeprecationWarning, r"deprecated.*version"):
warnings.warn("此方法在v2.0中deprecated", DeprecationWarning)
if __name__ == '__main__':
unittest.main(verbosity=2)
2.6 成员关系与类型断言详解
python
import unittest
import re
class TestMembershipAndTypeAssertions(unittest.TestCase):
"""
成员关系和类型断言详解
"""
def test_assertIn(self):
"""
assertIn(member, container, msg=None)
验证member在container中
"""
self.assertIn(2, [1, 2, 3])
self.assertIn('a', 'abc')
self.assertIn('key', {'key': 'value'})
self.assertIn(1, {1, 2, 3})
def test_assertNotIn(self):
"""
assertNotIn(member, container, msg=None)
验证member不在container中
"""
self.assertNotIn(4, [1, 2, 3])
self.assertNotIn('x', 'abc')
def test_assertIsInstance(self):
"""
assertIsInstance(obj, cls, msg=None)
验证obj是cls的实例(支持继承)
"""
self.assertIsInstance(5, int)
self.assertIsInstance("hello", str)
self.assertIsInstance([1, 2], list)
self.assertIsInstance(5, (int, float)) # 可以是类型元组
# 继承关系
class Animal:
pass
class Dog(Animal):
pass
dog = Dog()
self.assertIsInstance(dog, Dog)
self.assertIsInstance(dog, Animal) # 继承也成立
def test_assertNotIsInstance(self):
"""
assertNotIsInstance(obj, cls, msg=None)
验证obj不是cls的实例
"""
self.assertNotIsInstance("5", int)
self.assertNotIsInstance(5, str)
def test_assertRegex(self):
"""
assertRegex(text, regex, msg=None)
验证text匹配正则表达式regex
"""
# 验证邮箱格式
email = "user@example.com"
self.assertRegex(email, r"^[\w\.-]+@[\w\.-]+\.\w+$")
# 验证手机号(简化示例)
phone = "13800138000"
self.assertRegex(phone, r"^1[3-9]\d{9}$")
def test_assertNotRegex(self):
"""
assertNotRegex(text, regex, msg=None)
验证text不匹配正则表达式regex
"""
invalid_email = "not-an-email"
self.assertNotRegex(invalid_email, r"^[\w\.-]+@[\w\.-]+\.\w+$")
if __name__ == '__main__':
unittest.main(verbosity=2)
2.7 自定义断言方法
python
import unittest
class CustomTestCase(unittest.TestCase):
"""
自定义断言方法示例
通过继承TestCase添加项目特定的断言
"""
def assertIsValidEmail(self, email, msg=None):
"""
自定义断言:验证邮箱格式
"""
import re
pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
if not re.match(pattern, email):
standardMsg = f"'{email}' 不是有效的邮箱格式"
self.fail(self._formatMessage(msg, standardMsg))
def assertIsEmpty(self, iterable, msg=None):
"""
自定义断言:验证可迭代对象为空
"""
if len(iterable) != 0:
standardMsg = f"期望为空,实际包含 {len(iterable)} 个元素"
self.fail(self._formatMessage(msg, standardMsg))
def assertHasAttributes(self, obj, attributes, msg=None):
"""
自定义断言:验证对象具有所有指定属性
参数:
obj: 要检查的对象
attributes: 属性名列表
"""
missing = [attr for attr in attributes if not hasattr(obj, attr)]
if missing:
standardMsg = f"对象缺少属性: {', '.join(missing)}"
self.fail(self._formatMessage(msg, standardMsg))
class TestCustomAssertions(CustomTestCase):
"""
使用自定义断言的测试
"""
def test_valid_email(self):
self.assertIsValidEmail("user@example.com")
self.assertIsValidEmail("test.user@sub.domain.co.uk")
def test_invalid_email(self):
# 使用assertRaises验证失败情况
with self.assertRaises(AssertionError):
self.assertIsValidEmail("not-an-email")
def test_empty_list(self):
self.assertIsEmpty([])
def test_has_attributes(self):
class Person:
def __init__(self):
self.name = "张三"
self.age = 25
person = Person()
self.assertHasAttributes(person, ['name', 'age'])
if __name__ == '__main__':
unittest.main(verbosity=2)
第3章 测试夹具、清理与资源管理
3.1 夹具生命周期详解
方法级别
类级别
模块级别
setUpModule
模块内所有测试
tearDownModule
setUpClass
类内所有测试
tearDownClass
setUp
单个测试方法
tearDown
3.2 方法级别夹具:setUp/tearDown
python
import unittest
import tempfile
import os
class TestMethodFixtures(unittest.TestCase):
"""
方法级别夹具详解
setUp和tearDown在每个测试方法前后执行
"""
def setUp(self):
"""
每个测试方法执行前的准备工作
常见用途:
1. 创建临时文件/目录
2. 初始化测试数据
3. 建立数据库连接
4. 创建被测对象实例
"""
print(f"\n [setUp] 准备测试: {self._testMethodName}")
# 创建临时文件
self.temp_file = tempfile.NamedTemporaryFile(
mode='w',
delete=False,
suffix='.txt'
)
self.temp_file.write("测试数据\n")
self.temp_file.close()
# 初始化测试数据
self.test_data = {
'users': [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'}
]
}
def tearDown(self):
"""
每个测试方法执行后的清理工作
重要:即使测试失败,tearDown也会执行
用于确保资源被正确释放
"""
print(f" [tearDown] 清理测试: {self._testMethodName}")
# 清理临时文件
if hasattr(self, 'temp_file') and os.path.exists(self.temp_file.name):
os.unlink(self.temp_file.name)
def test_read_temp_file(self):
"""测试读取临时文件"""
with open(self.temp_file.name, 'r') as f:
content = f.read()
self.assertEqual(content, "测试数据\n")
def test_user_count(self):
"""测试用户数量"""
self.assertEqual(len(self.test_data['users']), 2)
if __name__ == '__main__':
unittest.main(verbosity=2)
3.3 类级别夹具:setUpClass/tearDownClass
python
import unittest
import sqlite3
import tempfile
import os
class TestClassFixtures(unittest.TestCase):
"""
类级别夹具详解
setUpClass和tearDownClass在类中所有测试前后各执行一次
注意:必须使用@classmethod装饰器
"""
@classmethod
def setUpClass(cls):
"""
类级别准备:所有测试方法之前执行一次
适用场景:
1. 创建共享的数据库连接
2. 启动测试服务器
3. 加载大型测试数据
4. 初始化昂贵的资源
"""
print("\n[setUpClass] 初始化类级别资源...")
# 创建临时数据库
cls.db_fd, cls.db_path = tempfile.mkstemp(suffix='.db')
# 建立数据库连接
cls.conn = sqlite3.connect(cls.db_path)
cls.cursor = cls.conn.cursor()
# 创建表结构
cls.cursor.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
)
''')
# 插入测试数据
cls.cursor.executemany('''
INSERT INTO users (name, email) VALUES (?, ?)
''', [
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Charlie', 'charlie@example.com')
])
cls.conn.commit()
print(f"[setUpClass] 数据库已创建: {cls.db_path}")
@classmethod
def tearDownClass(cls):
"""
类级别清理:所有测试方法之后执行一次
"""
print("\n[tearDownClass] 清理类级别资源...")
# 关闭数据库连接
cls.conn.close()
# 删除临时数据库文件
os.close(cls.db_fd)
os.unlink(cls.db_path)
print("[tearDownClass] 资源已清理")
def test_user_count(self):
"""测试用户总数"""
self.cursor.execute('SELECT COUNT(*) FROM users')
count = self.cursor.fetchone()[0]
self.assertEqual(count, 3)
def test_find_user_by_name(self):
"""测试按名称查找用户"""
self.cursor.execute(
'SELECT * FROM users WHERE name = ?',
('Alice',)
)
user = self.cursor.fetchone()
self.assertIsNotNone(user)
self.assertEqual(user[1], 'Alice')
def test_all_users_have_email(self):
"""测试所有用户都有邮箱"""
self.cursor.execute('SELECT COUNT(*) FROM users WHERE email IS NULL')
count = self.cursor.fetchone()[0]
self.assertEqual(count, 0)
if __name__ == '__main__':
unittest.main(verbosity=2)
3.4 模块级别夹具:setUpModule/tearDownModule
python
import unittest
# 模块级别的全局变量
shared_resource = None
connection_pool = None
def setUpModule():
"""
模块级别准备:模块中所有测试之前执行一次
适用场景:
1. 建立共享的连接池
2. 加载配置文件
3. 初始化日志系统
4. 启动外部服务
"""
global shared_resource, connection_pool
print("\n[setUpModule] 初始化模块级别资源...")
# 模拟初始化连接池
connection_pool = {
'connections': ['conn1', 'conn2', 'conn3'],
'max_size': 10
}
shared_resource = {
'initialized': True,
'config': {'debug': True, 'timeout': 30}
}
print("[setUpModule] 资源初始化完成")
def tearDownModule():
"""
模块级别清理:模块中所有测试之后执行一次
"""
global shared_resource, connection_pool
print("\n[tearDownModule] 清理模块级别资源...")
# 清理资源
connection_pool = None
shared_resource = None
print("[tearDownModule] 资源已清理")
class TestModuleLevelFixtures(unittest.TestCase):
"""测试模块级别夹具"""
def test_resource_initialized(self):
"""验证资源已初始化"""
self.assertIsNotNone(shared_resource)
self.assertTrue(shared_resource['initialized'])
def test_connection_pool_available(self):
"""验证连接池可用"""
self.assertIsNotNone(connection_pool)
self.assertEqual(len(connection_pool['connections']), 3)
class TestAnotherClass(unittest.TestCase):
"""另一个测试类,共享模块级别夹具"""
def test_config_loaded(self):
"""验证配置已加载"""
self.assertIn('config', shared_resource)
self.assertTrue(shared_resource['config']['debug'])
if __name__ == '__main__':
unittest.main(verbosity=2)
3.5 使用addCleanup进行资源清理
python
import unittest
import tempfile
import os
class TestAddCleanup(unittest.TestCase):
"""
addCleanup机制详解
优势:
1. 清理代码写在资源创建附近,提高可读性
2. 即使setUp中途失败,已注册的清理函数仍会执行
3. 支持多个清理函数,按LIFO顺序执行
"""
def test_with_addCleanup(self):
"""
使用addCleanup进行资源清理
"""
# 创建临时文件
temp_file = tempfile.NamedTemporaryFile(
mode='w',
delete=False,
suffix='.txt'
)
temp_file.write("重要数据")
temp_file.close()
# 立即注册清理函数
self.addCleanup(os.unlink, temp_file.name)
# 测试逻辑
with open(temp_file.name, 'r') as f:
content = f.read()
self.assertEqual(content, "重要数据")
# 文件会在tearDown后自动删除
def test_multiple_cleanups(self):
"""
多个清理函数按LIFO顺序执行
"""
cleanup_order = []
def cleanup1():
cleanup_order.append(1)
def cleanup2():
cleanup_order.append(2)
def cleanup3():
cleanup_order.append(3)
# 注册清理函数
self.addCleanup(cleanup1)
self.addCleanup(cleanup2)
self.addCleanup(cleanup3)
# 测试逻辑
pass
# 测试结束后,清理顺序将是:3, 2, 1
def test_cleanup_with_exception(self):
"""
即使测试失败,清理函数仍会执行
"""
temp_file = tempfile.NamedTemporaryFile(delete=False)
temp_file.close()
self.addCleanup(os.unlink, temp_file.name)
# 这个断言会失败
# 但temp_file仍会被清理
# self.assertTrue(False, "故意失败")
# 验证文件存在
self.assertTrue(os.path.exists(temp_file.name))
def test_cleanup_in_setup(self):
"""
在setUp中使用addCleanup
确保即使setUp失败也能清理资源
"""
# 模拟资源分配
resources = []
def acquire_resource(name):
resources.append(name)
self.addCleanup(release_resource, name)
def release_resource(name):
if name in resources:
resources.remove(name)
acquire_resource('resource1')
acquire_resource('resource2')
# 测试逻辑
self.assertEqual(len(resources), 2)
if __name__ == '__main__':
unittest.main(verbosity=2)
3.6 上下文管理器与资源管理
python
import unittest
from contextlib import contextmanager
class DatabaseConnection:
"""模拟数据库连接"""
def __init__(self, connection_string):
self.connection_string = connection_string
self.is_connected = False
def connect(self):
self.is_connected = True
print(f" 连接到: {self.connection_string}")
return self
def disconnect(self):
self.is_connected = False
print(f" 断开连接: {self.connection_string}")
def query(self, sql):
if not self.is_connected:
raise RuntimeError("未连接数据库")
return [f"result of: {sql}"]
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.disconnect()
return False # 不抑制异常
class TestContextManager(unittest.TestCase):
"""
使用上下文管理器管理资源
"""
def test_with_statement(self):
"""
使用with语句确保资源释放
"""
with DatabaseConnection("mysql://localhost/test") as db:
# 在上下文中使用资源
self.assertTrue(db.is_connected)
results = db.query("SELECT * FROM users")
self.assertEqual(len(results), 1)
# 退出上下文后,连接已断开
# 注意:这里无法直接访问db对象
def test_enterContext(self):
"""
Python 3.11+ 新增的enterContext方法
自动管理上下文管理器的生命周期
"""
# 对于Python 3.11+
try:
# db = self.enterContext(DatabaseConnection("postgres://localhost/test"))
# self.assertTrue(db.is_connected)
# results = db.query("SELECT 1")
pass
except AttributeError:
# Python 3.10及以下版本使用替代方案
self.skipTest("enterContext需要Python 3.11+")
@contextmanager
def managed_resource(self, name):
"""
自定义上下文管理器
"""
print(f" 获取资源: {name}")
resource = {'name': name, 'active': True}
try:
yield resource
finally:
resource['active'] = False
print(f" 释放资源: {name}")
def test_custom_context_manager(self):
"""测试自定义上下文管理器"""
with self.managed_resource("test_resource") as res:
self.assertTrue(res['active'])
self.assertEqual(res['name'], "test_resource")
if __name__ == '__main__':
unittest.main(verbosity=2)
第4章 Mock系统、patch机制与依赖隔离
4.1 Mock对象核心概念
Mock核心功能
方法调用记录
assert_called
返回值控制
return_value
副作用模拟
side_effect
属性管理
spec
Mock系统架构
Mock
MagicMock
NonCallableMock
AsyncMock
patch
装饰器/上下文管理器
依赖替换
4.2 Mock基础用法
python
import unittest
from unittest.mock import Mock, MagicMock
class TestMockBasics(unittest.TestCase):
"""
Mock对象基础用法详解
"""
def test_mock_basic(self):
"""
Mock基础:创建和调用
Mock对象可以模拟任何对象,
访问任何属性或方法都会自动创建新的Mock
"""
# 创建Mock对象
mock = Mock()
# 调用任意方法
result = mock.some_method()
# 默认返回新的Mock对象
self.assertIsInstance(result, Mock)
# 验证方法被调用
mock.some_method.assert_called_once()
def test_mock_return_value(self):
"""
设置返回值:return_value
"""
mock = Mock()
# 设置返回值
mock.calculate.return_value = 42
# 调用返回预设值
result = mock.calculate(10, 20)
self.assertEqual(result, 42)
# 验证调用参数
mock.calculate.assert_called_with(10, 20)
def test_mock_side_effect(self):
"""
side_effect:副作用控制
可用于:
1. 返回不同值
2. 抛出异常
3. 调用可迭代对象
"""
mock = Mock()
# 1. 返回不同值
mock.get_value.side_effect = [1, 2, 3]
self.assertEqual(mock.get_value(), 1)
self.assertEqual(mock.get_value(), 2)
self.assertEqual(mock.get_value(), 3)
# 2. 抛出异常
mock.risky_operation.side_effect = ValueError("出错了")
with self.assertRaises(ValueError):
mock.risky_operation()
# 3. 使用函数
def dynamic_return(x):
return x * 2
mock.process.side_effect = dynamic_return
self.assertEqual(mock.process(5), 10)
self.assertEqual(mock.process(3), 6)
def test_mock_call_args(self):
"""
验证调用参数
"""
mock = Mock()
# 多次调用
mock.process(1, 2, key='value')
mock.process(3, 4, key='other')
# 验证最后一次调用
self.assertEqual(mock.process.call_args, ((3, 4), {'key': 'other'}))
# 验证所有调用
expected_calls = [
((1, 2), {'key': 'value'}),
((3, 4), {'key': 'other'})
]
self.assertEqual(mock.process.call_args_list, expected_calls)
# 验证调用次数
self.assertEqual(mock.process.call_count, 2)
def test_mock_assertions(self):
"""
Mock断言方法
"""
mock = Mock()
# 调用mock
mock.method(1, 2, key='value')
# 各种断言方式
mock.method.assert_called() # 验证被调用过
mock.method.assert_called_once() # 验证只被调用一次
mock.method.assert_called_with(1, 2, key='value') # 验证特定参数
mock.method.assert_called_once_with(1, 2, key='value') # 验证只调用一次且参数正确
# 验证未被调用
mock.other_method.assert_not_called()
def test_mock_spec(self):
"""
spec:限制Mock的属性和方法
防止访问不存在的属性
"""
class RealClass:
def method1(self):
pass
def method2(self):
pass
# 创建带spec的Mock
mock = Mock(spec=RealClass)
# 可以访问spec中定义的方法
mock.method1()
mock.method2()
# 访问不存在的方法会报错
with self.assertRaises(AttributeError):
mock.nonexistent_method()
if __name__ == '__main__':
unittest.main(verbosity=2)
4.3 MagicMock与特殊方法
python
import unittest
from unittest.mock import Mock, MagicMock
class TestMagicMock(unittest.TestCase):
"""
MagicMock详解
MagicMock是Mock的子类,默认实现了所有魔术方法
"""
def test_magic_mock_vs_mock(self):
"""
MagicMock与Mock的区别
"""
regular_mock = Mock()
magic_mock = MagicMock()
# Mock不支持魔术方法
with self.assertRaises(TypeError):
len(regular_mock)
# MagicMock支持魔术方法
magic_mock.__len__.return_value = 5
self.assertEqual(len(magic_mock), 5)
def test_mock_as_context_manager(self):
"""
模拟上下文管理器
"""
mock = MagicMock()
# 配置上下文管理器行为
mock.__enter__.return_value = mock
mock.__exit__.return_value = False
with mock as m:
m.do_something()
# 验证上下文管理器被正确使用
mock.__enter__.assert_called_once()
mock.__exit__.assert_called_once()
mock.do_something.assert_called_once()
def test_mock_as_iterable(self):
"""
模拟可迭代对象
"""
mock = MagicMock()
# 配置迭代返回值
mock.__iter__.return_value = iter([1, 2, 3])
result = list(mock)
self.assertEqual(result, [1, 2, 3])
def test_mock_as_callable(self):
"""
模拟可调用对象
"""
mock = MagicMock()
# 直接调用mock
mock.return_value = "called"
result = mock("arg1", "arg2")
self.assertEqual(result, "called")
mock.assert_called_with("arg1", "arg2")
def test_mock_comparison(self):
"""
模拟比较操作
"""
mock = MagicMock()
# 配置比较方法
mock.__eq__.return_value = True
mock.__lt__.return_value = False
self.assertTrue(mock == "anything")
self.assertFalse(mock < 100)
if __name__ == '__main__':
unittest.main(verbosity=2)
4.4 patch装饰器详解
python
import unittest
from unittest.mock import patch, Mock
# 被测试的模块代码(模拟)
def external_api_call():
"""外部API调用"""
import requests
return requests.get("https://api.example.com/data")
def get_user_data(user_id):
"""获取用户数据"""
response = external_api_call()
return response.json()
class TestPatchDecorator(unittest.TestCase):
"""
patch装饰器详解
patch用于临时替换指定的对象
"""
@patch('__main__.external_api_call')
def test_patch_single(self, mock_api):
"""
单个patch装饰器
参数:
target: 要patch的对象路径(模块.对象名)
"""
# 配置mock返回值
mock_api.return_value = {'name': '张三', 'age': 25}
# 调用被测函数
result = get_user_data(1)
# 验证结果
self.assertEqual(result['name'], '张三')
# 验证mock被调用
mock_api.assert_called_once()
@patch('__main__.external_api_call')
@patch('builtins.print')
def test_patch_multiple(self, mock_print, mock_api):
"""
多个patch装饰器
注意:装饰器从内向外应用,参数从右向左传递
"""
mock_api.return_value = 'data'
print(get_user_data(1))
# 验证两个mock都被调用
mock_api.assert_called_once()
mock_print.assert_called_once()
@patch('__main__.external_api_call', return_value={'status': 'ok'})
def test_patch_with_kwargs(self, mock_api):
"""
在装饰器中直接设置return_value
"""
result = get_user_data(1)
self.assertEqual(result['status'], 'ok')
@patch('__main__.external_api_call', autospec=True)
def test_patch_autospec(self, mock_api):
"""
autospec=True:保持原对象的签名
防止调用错误的参数
"""
mock_api.return_value = {'data': []}
result = get_user_data(1)
self.assertEqual(result, {'data': []})
# autospec会检查调用参数
# 如果external_api_call有特定参数要求,会验证参数
if __name__ == '__main__':
unittest.main(verbosity=2)
4.5 patch上下文管理器
python
import unittest
from unittest.mock import patch, Mock
def read_config():
"""读取配置文件"""
with open('/etc/config.txt', 'r') as f:
return f.read()
def process_data():
"""处理数据"""
import json
data = json.loads('{"key": "value"}')
return data
class TestPatchContextManager(unittest.TestCase):
"""
patch作为上下文管理器
适用于:
1. 只需要在代码块中临时替换
2. 需要精细控制patch的范围
"""
def test_patch_context_manager(self):
"""
使用with语句进行patch
"""
mock_data = "mocked config content"
with patch('builtins.open') as mock_open:
# 配置mock文件对象
mock_file = Mock()
mock_file.read.return_value = mock_data
mock_open.return_value.__enter__.return_value = mock_file
# 调用被测函数
result = read_config()
# 验证结果
self.assertEqual(result, mock_data)
# 验证open被正确调用
mock_open.assert_called_once_with('/etc/config.txt', 'r')
# 退出with块后,patch自动恢复
def test_patch_multiple_context_managers(self):
"""
多个patch上下文管理器
"""
with patch('json.loads') as mock_loads, \
patch('builtins.print') as mock_print:
mock_loads.return_value = {'mocked': True}
result = process_data()
self.assertEqual(result, {'mocked': True})
mock_loads.assert_called_once()
def test_patch_start_stop(self):
"""
手动控制patch的开始和结束
"""
# 创建patch对象
patcher = patch('__main__.read_config')
mock_func = patcher.start()
try:
mock_func.return_value = "patched"
result = read_config()
self.assertEqual(result, "patched")
finally:
# 确保patch被恢复
patcher.stop()
if __name__ == '__main__':
unittest.main(verbosity=2)
4.6 patch.object与patch.dict
python
import unittest
from unittest.mock import patch, Mock
class Database:
"""数据库类"""
connection = None
@classmethod
def connect(cls):
cls.connection = "real_connection"
return cls.connection
@classmethod
def query(cls, sql):
# 注意:这里假设connection是一个具有execute方法的数据库连接对象
# 在实际测试中,我们会使用patch.object来模拟这个方法
return cls.connection.execute(sql)
class TestPatchVariants(unittest.TestCase):
"""
patch的其他变体
"""
def test_patch_object(self):
"""
patch.object:patch对象的方法
语法:patch.object(target, attribute, **kwargs)
"""
with patch.object(Database, 'connect', return_value='mock_connection'):
result = Database.connect()
self.assertEqual(result, 'mock_connection')
def test_patch_dict(self):
"""
patch.dict:临时修改字典
适用于:os.environ, 配置字典等
"""
import os
# 临时修改环境变量
with patch.dict('os.environ', {'TEST_VAR': 'test_value', 'API_KEY': 'secret'}):
self.assertEqual(os.environ['TEST_VAR'], 'test_value')
self.assertEqual(os.environ['API_KEY'], 'secret')
# 退出后恢复原值
self.assertNotIn('TEST_VAR', os.environ)
def test_patch_dict_clear(self):
"""
patch.dict清空并设置新值
"""
original = {'keep': 'value'}
with patch.dict(original, {'new': 'data'}, clear=True):
self.assertEqual(original, {'new': 'data'})
# 恢复原值
self.assertEqual(original, {'keep': 'value'})
if __name__ == '__main__':
unittest.main(verbosity=2)
4.7 Mock实战:数据库测试
python
import unittest
from unittest.mock import Mock, patch, MagicMock
import sqlite3
class UserRepository:
"""用户仓库类"""
def __init__(self, db_connection):
self.db = db_connection
def get_user_by_id(self, user_id):
"""根据ID获取用户"""
cursor = self.db.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'name': row[1],
'email': row[2]
}
return None
def create_user(self, name, email):
"""创建用户"""
cursor = self.db.cursor()
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(name, email)
)
self.db.commit()
return cursor.lastrowid
def delete_user(self, user_id):
"""删除用户"""
cursor = self.db.cursor()
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
self.db.commit()
return cursor.rowcount > 0
class TestUserRepository(unittest.TestCase):
"""
使用Mock测试数据库操作
"""
def setUp(self):
"""创建mock数据库连接"""
self.mock_db = Mock()
self.mock_cursor = Mock()
self.mock_db.cursor.return_value = self.mock_cursor
self.repo = UserRepository(self.mock_db)
def test_get_user_by_id_found(self):
"""测试获取存在的用户"""
# 配置mock返回值
self.mock_cursor.fetchone.return_value = (1, '张三', 'zhangsan@example.com')
user = self.repo.get_user_by_id(1)
# 验证结果
self.assertIsNotNone(user)
self.assertEqual(user['id'], 1)
self.assertEqual(user['name'], '张三')
# 验证SQL执行
self.mock_cursor.execute.assert_called_once_with(
"SELECT * FROM users WHERE id = ?",
(1,)
)
def test_get_user_by_id_not_found(self):
"""测试获取不存在的用户"""
self.mock_cursor.fetchone.return_value = None
user = self.repo.get_user_by_id(999)
self.assertIsNone(user)
def test_create_user(self):
"""测试创建用户"""
self.mock_cursor.lastrowid = 42
user_id = self.repo.create_user('李四', 'lisi@example.com')
self.assertEqual(user_id, 42)
self.mock_cursor.execute.assert_called_once()
self.mock_db.commit.assert_called_once()
def test_delete_user_success(self):
"""测试删除用户成功"""
self.mock_cursor.rowcount = 1
result = self.repo.delete_user(1)
self.assertTrue(result)
self.mock_db.commit.assert_called_once()
def test_delete_user_not_found(self):
"""测试删除不存在的用户"""
self.mock_cursor.rowcount = 0
result = self.repo.delete_user(999)
self.assertFalse(result)
if __name__ == '__main__':
unittest.main(verbosity=2)
第5章 参数化测试与数据驱动
5.1 subTest实现参数化
python
import unittest
class Calculator:
"""计算器类"""
def add(self, a, b):
return a + b
def divide(self, a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
class TestWithSubTest(unittest.TestCase):
"""
使用subTest进行参数化测试
subTest是Python 3.4+引入的特性
允许在一个测试方法中运行多个子测试
"""
def test_add_with_subtest(self):
"""
使用subTest进行参数化测试
优势:
1. 一个子测试失败不影响其他子测试
2. 清晰的失败信息,显示具体参数
"""
test_cases = [
(1, 1, 2), # 正数
(-1, -1, -2), # 负数
(0, 5, 5), # 零
(-1, 1, 0), # 正负相加
(100, 200, 300) # 大数
]
calc = Calculator()
for a, b, expected in test_cases:
with self.subTest(a=a, b=b, expected=expected):
result = calc.add(a, b)
self.assertEqual(result, expected)
def test_divide_with_subtest(self):
"""
除法测试:包含正常和异常情况
"""
# 正常情况
normal_cases = [
(10, 2, 5.0),
(7, 2, 3.5),
(0, 5, 0.0),
(-10, 2, -5.0),
]
calc = Calculator()
for a, b, expected in normal_cases:
with self.subTest(a=a, b=b, case="normal"):
result = calc.divide(a, b)
self.assertEqual(result, expected)
# 异常情况
error_cases = [
(10, 0, ValueError),
(-5, 0, ValueError),
]
for a, b, expected_exception in error_cases:
with self.subTest(a=a, b=b, case="error"):
with self.assertRaises(expected_exception):
calc.divide(a, b)
def test_subtest_with_iteration(self):
"""
在迭代中使用subTest
"""
users = [
{'id': 1, 'name': 'Alice', 'active': True},
{'id': 2, 'name': 'Bob', 'active': False},
{'id': 3, 'name': 'Charlie', 'active': True},
]
for user in users:
with self.subTest(user_id=user['id'], name=user['name']):
# 每个用户都会作为独立的子测试
self.assertIn('id', user)
self.assertIn('name', user)
self.assertIsInstance(user['active'], bool)
if __name__ == '__main__':
unittest.main(verbosity=2)
5.2 第三方库parameterized的使用
python
import unittest
# 注意:需要安装 parameterized
# pip install parameterized
try:
from parameterized import parameterized
HAS_PARAMETERIZED = True
except ImportError:
HAS_PARAMETERIZED = False
class Calculator:
"""计算器类"""
def add(self, a, b):
return a + b
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
@unittest.skipUnless(HAS_PARAMETERIZED, "需要安装parameterized库")
class TestWithParameterized(unittest.TestCase):
"""
使用parameterized库进行参数化测试
优势:
1. 更简洁的语法
2. 每个参数组合都是独立的测试方法
3. 更好的测试报告
"""
@parameterized.expand([
(1, 1, 2),
(-1, -1, -2),
(0, 5, 5),
(-1, 1, 0),
(100, 200, 300),
])
def test_add(self, a, b, expected):
"""参数化测试加法"""
calc = Calculator()
result = calc.add(a, b)
self.assertEqual(result, expected)
@parameterized.expand([
("positive", 10, 2, 5.0),
("negative", -10, 2, -5.0),
("fraction", 7, 2, 3.5),
])
def test_divide(self, name, a, b, expected):
"""参数化测试除法,带测试名称"""
calc = Calculator()
result = calc.divide(a, b)
self.assertEqual(result, expected)
if __name__ == '__main__':
unittest.main(verbosity=2)
5.3 动态跳过测试:skipTest()方法
python
import unittest
import sys
class TestSkipTest(unittest.TestCase):
"""
skipTest(reason)方法详解
与@skip装饰器不同,skipTest()在运行时动态决定是否跳过
适用于基于运行时条件的跳过
"""
def test_skip_based_on_condition(self):
"""
基于条件动态跳过
"""
# 检查Python版本
if sys.version_info < (3, 8):
self.skipTest("此测试需要Python 3.8+")
# 如果未跳过,执行测试
self.assertTrue(hasattr(str, 'removeprefix'))
def test_skip_based_on_environment(self):
"""
基于环境变量跳过
"""
import os
# 检查是否设置了特定环境变量
if not os.environ.get('RUN_INTEGRATION_TESTS'):
self.skipTest("跳过集成测试(设置RUN_INTEGRATION_TESTS=1启用)")
# 执行集成测试
self.assertTrue(True)
def test_skip_based_on_resource_availability(self):
"""
基于资源可用性跳过
"""
# 检查数据库连接是否可用
db_available = False # 模拟检查
if not db_available:
self.skipTest("数据库不可用,跳过测试")
# 执行数据库测试
self.assertTrue(True)
def test_skip_in_loop(self):
"""
在循环中选择性跳过
"""
test_data = [
(1, "valid"),
(2, "valid"),
(3, "unsupported"), # 这组数据跳过
(4, "valid"),
]
for value, status in test_data:
with self.subTest(value=value):
if status == "unsupported":
self.skipTest(f"值 {value} 当前不支持")
self.assertIsInstance(value, int)
if __name__ == '__main__':
unittest.main(verbosity=2)
第6章 测试组织、发现与加载机制
6.1 测试发现机制
python
import unittest
"""
unittest测试发现机制详解
命令行使用:
python -m unittest discover -s tests -p "test_*.py" -v
参数说明:
-s, --start-directory: 搜索起始目录,默认为当前目录
-p, --pattern: 匹配模式,默认为"test*.py"
-t, --top-level-directory: 项目根目录
"""
# 程序化测试发现
def discover_tests():
"""
程序化方式发现测试
"""
# 创建测试加载器
loader = unittest.TestLoader()
# 发现测试
start_dir = '.' # 起始目录
pattern = 'test_*.py' # 匹配模式
suite = loader.discover(start_dir, pattern)
return suite
if __name__ == '__main__':
# 运行发现的测试
suite = discover_tests()
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
6.2 测试加载策略
python
import unittest
class TestLoadingStrategies(unittest.TestCase):
"""
测试加载策略详解
"""
def test_load_from_test_case(self):
"""
从测试类加载
"""
loader = unittest.TestLoader()
# 加载单个测试类
suite = loader.loadTestsFromTestCase(TestLoadingStrategies)
self.assertIsInstance(suite, unittest.TestSuite)
def test_load_from_module(self):
"""
从模块加载
"""
loader = unittest.TestLoader()
# 加载当前模块的所有测试
import __main__
suite = loader.loadTestsFromModule(__main__)
self.assertIsInstance(suite, unittest.TestSuite)
def test_load_from_name(self):
"""
从名称加载
支持的名称格式:
- 'module'
- 'module.TestClass'
- 'module.TestClass.test_method'
"""
loader = unittest.TestLoader()
# 加载特定测试方法
# suite = loader.loadTestsFromName('__main__.TestLoadingStrategies.test_load_from_name')
# 加载测试类
# suite = loader.loadTestsFromName('__main__.TestLoadingStrategies')
pass
def test_load_from_names(self):
"""
从多个名称加载
"""
loader = unittest.TestLoader()
names = [
'__main__.TestLoadingStrategies.test_load_from_test_case',
'__main__.TestLoadingStrategies.test_load_from_module',
]
suite = loader.loadTestsFromNames(names)
self.assertIsInstance(suite, unittest.TestSuite)
if __name__ == '__main__':
unittest.main(verbosity=2)
6.3 组织大型项目测试
python
import unittest
"""
大型项目测试组织结构
推荐目录结构:
project/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── module1.py
│ └── module2.py
├── tests/
│ ├── __init__.py
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── integration/
│ │ ├── __init__.py
│ │ └── test_integration.py
│ ├── e2e/
│ │ ├── __init__.py
│ │ └── test_e2e.py
│ ├── fixtures/
│ │ └── data.json
│ └── utils/
│ ├── __init__.py
│ └── test_helpers.py
├── requirements-test.txt
└── run_tests.py
测试目录结构最佳实践:
1. **按测试类型组织**:
- `tests/`:存放所有测试
- `tests/integration/`:集成测试
- `tests/e2e/`:端到端测试
- `tests/fixtures/`:测试数据和夹具
- `tests/utils/`:测试辅助工具
2. **测试文件命名**:
- 单元测试:`test_<模块名>.py`
- 集成测试:`test_<集成点>.py`
- 端到端测试:`test_<场景>.py`
3. **测试类命名**:
- 单元测试:`Test<模块名>`
- 集成测试:`Test<集成点>Integration`
- 端到端测试:`Test<场景>E2E`
4. **测试方法命名**:
- 遵循`test_<被测方法>_<测试场景>_<预期结果>`模式
- 使用描述性名称,清晰表达测试目的
5. **配置文件**:
- `requirements-test.txt`:测试依赖
- `.coveragerc`:覆盖率配置
- `pytest.ini`:如果使用pytest
6. **测试辅助文件**:
- `tests/utils/`:测试工具函数
- `tests/fixtures/`:测试数据和固定装置
- `tests/__init__.py`:测试包配置
"""
# tests/__init__.py 示例
"""
测试包初始化文件
可以在这里设置:
1. 共享的测试夹具
2. 测试配置
3. 自定义TestCase基类
4. 测试常量和工具函数
"""
import unittest
import os
import sys
# 添加src目录到Python路径,确保测试可以导入被测模块
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
class BaseTestCase(unittest.TestCase):
"""
项目自定义的测试基类
提供所有测试共享的功能:
- 通用的setUp和tearDown方法
- 测试辅助方法
- 共享的测试数据
"""
def setUp(self):
"""所有测试的通用准备"""
super().setUp()
# 通用设置
self.test_data_dir = os.path.join(os.path.dirname(__file__), 'fixtures')
def tearDown(self):
"""所有测试的通用清理"""
# 通用清理
super().tearDown()
def get_test_data(self, filename):
"""获取测试数据文件的路径"""
return os.path.join(self.test_data_dir, filename)
# run_tests.py 示例
def run_all_tests():
"""
统一测试入口
支持不同类型测试的运行:
- 全部测试
- 仅单元测试
- 仅集成测试
- 仅端到端测试
"""
import argparse
# 解析命令行参数
parser = argparse.ArgumentParser(description='运行测试')
parser.add_argument('--type', choices=['all', 'unit', 'integration', 'e2e'], default='all',
help='测试类型')
parser.add_argument('--verbose', '-v', action='store_true',
help='详细输出')
args = parser.parse_args()
# 根据测试类型设置发现目录
if args.type == 'unit':
start_dir = 'tests'
pattern = 'test_*.py'
elif args.type == 'integration':
start_dir = 'tests/integration'
pattern = 'test_*.py'
elif args.type == 'e2e':
start_dir = 'tests/e2e'
pattern = 'test_*.py'
else: # all
start_dir = 'tests'
pattern = 'test_*.py'
# 配置测试发现
loader = unittest.TestLoader()
# 发现测试
suite = loader.discover(start_dir, pattern)
# 配置测试运行器
runner = unittest.TextTestRunner(
verbosity=2 if args.verbose else 1,
failfast=False, # 遇到失败不停止
buffer=True # 捕获输出
)
# 运行测试
print(f"运行{args.type}测试...")
result = runner.run(suite)
# 返回退出码
return 0 if result.wasSuccessful() else 1
if __name__ == '__main__':
exit(run_all_tests())
第7章 测试执行、结果处理与报告
7.1 测试结果处理
python
import unittest
class CustomTestResult(unittest.TestResult):
"""
自定义测试结果类
可以自定义:
1. 结果存储方式
2. 结果输出格式
3. 失败/错误处理逻辑
"""
def __init__(self, stream=None, descriptions=None, verbosity=None):
super().__init__(stream, descriptions, verbosity)
self.successes = []
def addSuccess(self, test):
"""记录成功的测试"""
super().addSuccess(test)
self.successes.append(test)
print(f"✓ {test}")
def addFailure(self, test, err):
"""记录失败的测试"""
super().addFailure(test, err)
print(f"✗ {test}")
def addError(self, test, err):
"""记录出错的测试"""
super().addError(test, err)
print(f"⚠ {test}")
def addSkip(self, test, reason):
"""记录跳过的测试"""
super().addSkip(test, reason)
print(f"⊘ {test} (原因: {reason})")
def print_summary(self):
"""打印测试摘要"""
print("\n" + "=" * 60)
print(f"测试摘要:")
print(f" 成功: {len(self.successes)}")
print(f" 失败: {len(self.failures)}")
print(f" 错误: {len(self.errors)}")
print(f" 跳过: {len(self.skipped)}")
print("=" * 60)
class TestResultDemo(unittest.TestCase):
"""测试结果演示"""
def test_success(self):
self.assertTrue(True)
@unittest.skip("演示跳过")
def test_skip(self):
pass
if __name__ == '__main__':
# 使用自定义结果类
suite = unittest.TestLoader().loadTestsFromTestCase(TestResultDemo)
result = CustomTestResult()
suite.run(result)
result.print_summary()
7.2 生成HTML测试报告
python
import unittest
"""
生成HTML测试报告
需要安装第三方库:
pip install HtmlTestRunner
HtmlTestRunner是一个第三方库,用于生成美观的HTML格式测试报告。
"""
try:
import HtmlTestRunner
HAS_HTML_RUNNER = True
except ImportError:
HAS_HTML_RUNNER = False
class TestForHTMLReport(unittest.TestCase):
"""用于生成HTML报告的测试示例"""
def test_pass_1(self):
"""测试通过示例1"""
self.assertEqual(1 + 1, 2)
def test_pass_2(self):
"""测试通过示例2"""
self.assertTrue(True)
def test_pass_3(self):
"""测试通过示例3"""
self.assertIn(1, [1, 2, 3])
if __name__ == '__main__' and HAS_HTML_RUNNER:
# 生成HTML报告
unittest.main(
testRunner=HtmlTestRunner.HTMLTestRunner(
output='test_reports', # 报告输出目录
report_name='unittest_report', # 报告文件名
report_title='Unittest测试报告', # 报告标题
combine_reports=True, # 合并多个测试套件的报告
add_timestamp=False, # 是否添加时间戳到报告文件名
verbosity=2, # 输出详细程度
failfast=False, # 遇到失败是否停止
buffer=True # 是否捕获输出
)
)
elif __name__ == '__main__':
print("请安装HtmlTestRunner: pip install HtmlTestRunner")
unittest.main(verbosity=2)
"""
HTML测试报告配置选项详解:
1. **output**:报告输出目录,默认为' Reports'
2. **report_name**:报告文件名,默认为'test_report'
3. **report_title**:报告标题,默认为'Unit Test Report'
4. **combine_reports**:是否合并多个测试套件的报告,默认为False
5. **add_timestamp**:是否在报告文件名后添加时间戳,默认为True
6. **verbosity**:输出详细程度,1为简要,2为详细
7. **failfast**:遇到第一个失败是否停止测试,默认为False
8. **buffer**:是否捕获标准输出和标准错误,默认为False
HTML报告内容包含:
1. **测试摘要**:总测试数、通过数、失败数、错误数、跳过数
2. **测试详情**:每个测试方法的执行状态、执行时间、失败/错误信息
3. **测试统计**:通过率、执行时间等统计信息
4. **环境信息**:Python版本、操作系统等
5. **错误详情**:详细的错误堆栈信息
使用建议:
- 在CI/CD流程中使用HTML报告,便于查看测试结果
- 结合coverage库生成覆盖率报告,更全面地了解测试覆盖情况
- 定期归档测试报告,作为项目质量的参考
"""
第8章 测试控制、高级特性与扩展
8.1 跳过测试装饰器
python
import unittest
import sys
import platform
class TestSkipDecorators(unittest.TestCase):
"""
跳过测试装饰器详解
"""
@unittest.skip("无条件跳过此测试")
def test_skip_unconditionally(self):
"""无条件跳过的测试"""
self.fail("这不会被执行")
@unittest.skipIf(sys.version_info < (3, 8), "需要Python 3.8+")
def test_skip_if_python_version(self):
"""条件跳过:Python版本"""
# 只有Python 3.8+才会执行
self.assertTrue(hasattr(str, 'removeprefix'))
@unittest.skipUnless(platform.system() == 'Windows', "仅在Windows上运行")
def test_skip_unless_windows(self):
"""条件跳过:操作系统"""
import os
self.assertTrue(os.path.exists('C:\\'))
@unittest.expectedFailure
def test_expected_failure(self):
"""
预期失败的测试
如果测试失败,标记为expected failure
如果测试通过,标记为unexpected success
"""
self.assertEqual(1, 2) # 这应该会失败
if __name__ == '__main__':
unittest.main(verbosity=2)
8.2 异步测试支持
python
import unittest
import asyncio
# Python 3.8+ 支持IsolatedAsyncioTestCase
try:
from unittest import IsolatedAsyncioTestCase
HAS_ASYNC_SUPPORT = True
except ImportError:
HAS_ASYNC_SUPPORT = False
IsolatedAsyncioTestCase = unittest.TestCase
async def async_add(a, b):
"""异步加法函数"""
await asyncio.sleep(0.1) # 模拟异步操作
return a + b
async def async_fetch_data():
"""异步获取数据"""
await asyncio.sleep(0.1)
return {'status': 'success', 'data': [1, 2, 3]}
@unittest.skipUnless(HAS_ASYNC_SUPPORT, "需要Python 3.8+")
class TestAsyncFunctions(IsolatedAsyncioTestCase):
"""
异步函数测试
使用IsolatedAsyncioTestCase可以:
1. 直接测试async函数
2. 每个测试有独立的事件循环
"""
async def test_async_add(self):
"""测试异步加法"""
result = await async_add(2, 3)
self.assertEqual(result, 5)
async def test_async_fetch_data(self):
"""测试异步数据获取"""
result = await async_fetch_data()
self.assertEqual(result['status'], 'success')
self.assertEqual(len(result['data']), 3)
async def test_async_exception(self):
"""测试异步异常"""
async def raise_error():
await asyncio.sleep(0.01)
raise ValueError("异步错误")
with self.assertRaises(ValueError):
await raise_error()
# Python 3.7及以下版本的替代方案
class TestAsyncWithLoop(unittest.TestCase):
"""
使用事件循环测试异步代码(兼容旧版本)
"""
def run_async(self, coro):
"""运行异步协程"""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
def test_async_add(self):
"""测试异步加法"""
result = self.run_async(async_add(2, 3))
self.assertEqual(result, 5)
if __name__ == '__main__':
unittest.main(verbosity=2)
第9章 测试质量、覆盖率与工程实践
9.1 代码覆盖率分析
python
import unittest
"""
代码覆盖率分析
需要安装coverage库:
pip install coverage
常用命令:
coverage run -m unittest discover
coverage report
coverage html
coverage xml
"""
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("除数不能为零")
return a / b
def power(self, base, exponent):
"""幂运算"""
return base ** exponent
class TestCalculatorCoverage(unittest.TestCase):
"""
Calculator类的测试
运行覆盖率分析:
coverage run -m unittest test_coverage.TestCalculatorCoverage
coverage report -m
"""
def setUp(self):
self.calc = Calculator()
def test_add(self):
self.assertEqual(self.calc.add(2, 3), 5)
def test_subtract(self):
self.assertEqual(self.calc.subtract(5, 3), 2)
def test_multiply(self):
self.assertEqual(self.calc.multiply(4, 3), 12)
def test_divide(self):
self.assertEqual(self.calc.divide(10, 2), 5.0)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
# 注意:power方法没有被测试,覆盖率会显示这一点
if __name__ == '__main__':
unittest.main(verbosity=2)
9.2 测试最佳实践
python
import unittest
from unittest.mock import Mock, patch
"""
测试最佳实践
"""
class TestBestPractices(unittest.TestCase):
"""
测试最佳实践示例
"""
def test_arrange_act_assert_pattern(self):
"""
AAA模式:Arrange-Act-Assert
清晰的测试结构:
1. Arrange: 准备测试数据和条件
2. Act: 执行被测操作
3. Assert: 验证结果
"""
# Arrange
numbers = [1, 2, 3, 4, 5]
# Act
result = sum(numbers)
# Assert
self.assertEqual(result, 15)
def test_one_assert_per_concept(self):
"""
每个测试只验证一个概念
不好的做法:在一个测试中验证多个不相关的事情
好的做法:将测试拆分为多个小测试
"""
# 好的做法:只验证一个概念
user = {'name': 'Alice', 'age': 30}
self.assertEqual(user['name'], 'Alice')
def test_meaningful_test_names(self):
"""
有意义的测试名称
测试名称应该说明:
1. 被测功能
2. 测试条件
3. 预期结果
测试命名规范最佳实践:
- 始终使用test_前缀,unittest会自动识别这些方法为测试
- 使用描述性的名称,清晰表达测试的目的
- 使用下划线分隔单词,提高可读性
- 遵循"test_被测方法_测试场景_预期结果"的命名模式
- 避免使用数字编号(如test_1, test_2),这会降低可读性
- 保持名称简洁但信息丰富
好的测试名称示例:
- test_add_positive_numbers
- test_divide_by_zero_raises_error
- test_user_registration_with_valid_data
- test_login_with_invalid_credentials
"""
pass # 名称本身就是文档
def test_independent_tests(self):
"""
测试之间相互独立
每个测试应该可以单独运行
测试执行顺序不应该影响结果
测试隔离性实践方法:
1. 使用setUp/tearDown方法:在每个测试前后创建和清理测试环境
2. 使用局部变量:避免使用全局变量存储测试状态
3. 隔离外部依赖:使用Mock模拟外部服务和数据库操作
4. 事务回滚:对于数据库测试,使用事务并在测试后回滚
5. 临时资源:使用临时文件和目录,测试后自动清理
6. 避免测试顺序依赖:不要假设测试按特定顺序执行
7. 每个测试方法只测试一个概念:保持测试的单一职责
8. 清理共享状态:确保测试不会留下影响其他测试的状态
隔离性测试的优势:
- 测试结果更加可靠和可预测
- 更容易定位和调试失败的测试
- 支持并行执行测试,提高测试速度
- 减少测试之间的意外依赖
"""
# 使用setUp创建独立的状态
local_data = {'counter': 0}
local_data['counter'] += 1
self.assertEqual(local_data['counter'], 1)
def test_fast_tests(self):
"""
测试应该快速执行
使用Mock避免:
1. 真实的数据库操作
2. 网络请求
3. 文件I/O
4. 长时间计算
"""
# 使用Mock模拟耗时操作
mock_service = Mock()
mock_service.get_data.return_value = {'cached': True}
result = mock_service.get_data()
self.assertTrue(result['cached'])
if __name__ == '__main__':
unittest.main(verbosity=2)
9.3 测试金字塔概念
测试金字塔是一种测试策略模型,它建议测试应该按照不同的层次组织,从底层到顶层数量逐渐减少:
测试特性
测试金字塔
单元测试
70-80%
集成测试
15-20%
端到端测试
5-10%
快速执行
高隔离性
低成本
中等执行速度
中等隔离性
中等成本
慢速执行
低隔离性
高成本
测试金字塔各层级详解
-
单元测试(底层)
- 比例:70-80%
- 特点:测试单个函数或类的行为,隔离外部依赖
- 工具:unittest、pytest
- 优势:执行速度快,易于定位问题,维护成本低
- 示例:测试Calculator类的add方法
-
集成测试(中层)
- 比例:15-20%
- 特点:测试多个组件之间的交互
- 工具:unittest + Mock、pytest
- 优势:验证组件协作,发现集成问题
- 示例:测试UserRepository与数据库的交互
-
端到端测试(顶层)
- 比例:5-10%
- 特点:测试整个应用的完整流程
- 工具:Selenium、Cypress等
- 优势:验证用户场景,确保系统整体功能
- 示例:测试用户登录到完成购买的完整流程
测试金字塔的优势
- 平衡测试覆盖:确保关键功能有足够的测试覆盖
- 优化测试速度:大量快速的单元测试确保基本功能正常
- 降低维护成本:底层测试更容易维护和更新
- 快速反馈:单元测试失败可以快速定位问题
- 风险控制:上层测试捕获集成和系统级问题
实践建议
- 优先编写单元测试:确保核心功能的正确性
- 适度编写集成测试:验证组件间的交互
- 谨慎编写端到端测试:关注核心用户流程
- 保持测试金字塔比例:避免过多的端到端测试导致测试套件运行缓慢
- 持续评估测试策略:根据项目特点调整测试比例
第10章 生产环境最佳实践与实战
10.1 CI/CD集成
yaml
# .github/workflows/test.yml
# GitHub Actions配置示例
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install coverage
- name: Run tests
run: |
python -m unittest discover -v
- name: Run tests with coverage
run: |
coverage run -m unittest discover
coverage report
coverage xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: false
10.2 数据库测试最佳实践
python
import unittest
import sqlite3
import tempfile
import os
class TestDatabaseBestPractices(unittest.TestCase):
"""
数据库测试最佳实践
原则:
1. 使用事务回滚,不提交真实数据
2. 每个测试后清理数据
3. 使用内存数据库加速测试
4. 使用工厂模式创建测试数据
"""
@classmethod
def setUpClass(cls):
"""创建共享的数据库连接"""
# 使用内存数据库
cls.conn = sqlite3.connect(':memory:')
cls.cursor = cls.conn.cursor()
# 创建表
cls.cursor.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
''')
cls.conn.commit()
@classmethod
def tearDownClass(cls):
"""关闭数据库连接"""
cls.conn.close()
def setUp(self):
"""每个测试前清理数据"""
self.cursor.execute("DELETE FROM users")
self.conn.commit()
def create_user(self, name, email):
"""工厂方法:创建用户"""
self.cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
(name, email)
)
self.conn.commit()
return self.cursor.lastrowid
def test_create_user(self):
"""测试创建用户"""
user_id = self.create_user('Alice', 'alice@example.com')
self.assertIsNotNone(user_id)
def test_user_count(self):
"""测试用户计数"""
self.create_user('User1', 'user1@example.com')
self.create_user('User2', 'user2@example.com')
self.cursor.execute("SELECT COUNT(*) FROM users")
count = self.cursor.fetchone()[0]
self.assertEqual(count, 2)
if __name__ == '__main__':
unittest.main(verbosity=2)
10.3 Web应用测试
python
import unittest
from unittest.mock import Mock, patch
"""
Web应用测试示例
对于Flask/Django等Web框架,通常使用:
1. 测试客户端发送请求
2. Mock外部服务
3. 验证响应状态和内容
"""
class MockWebApp:
"""模拟Web应用"""
def __init__(self):
self.users = {}
self.next_id = 1
def create_user(self, name, email):
"""创建用户"""
user_id = self.next_id
self.users[user_id] = {'id': user_id, 'name': name, 'email': email}
self.next_id += 1
return self.users[user_id]
def get_user(self, user_id):
"""获取用户"""
return self.users.get(user_id)
def delete_user(self, user_id):
"""删除用户"""
if user_id in self.users:
del self.users[user_id]
return True
return False
class TestWebApplication(unittest.TestCase):
"""
Web应用测试
"""
def setUp(self):
"""每个测试前初始化应用"""
self.app = MockWebApp()
def test_create_user(self):
"""测试创建用户端点"""
user = self.app.create_user('Alice', 'alice@example.com')
self.assertEqual(user['name'], 'Alice')
self.assertEqual(user['email'], 'alice@example.com')
self.assertIn('id', user)
def test_get_user(self):
"""测试获取用户端点"""
created = self.app.create_user('Bob', 'bob@example.com')
user = self.app.get_user(created['id'])
self.assertIsNotNone(user)
self.assertEqual(user['name'], 'Bob')
def test_get_nonexistent_user(self):
"""测试获取不存在的用户"""
user = self.app.get_user(999)
self.assertIsNone(user)
def test_delete_user(self):
"""测试删除用户端点"""
created = self.app.create_user('Charlie', 'charlie@example.com')
result = self.app.delete_user(created['id'])
self.assertTrue(result)
self.assertIsNone(self.app.get_user(created['id']))
if __name__ == '__main__':
unittest.main(verbosity=2)
10.4 常见测试陷阱与避免方法
python
import unittest
from unittest.mock import Mock, patch
class TestAntiPatterns(unittest.TestCase):
"""
常见测试陷阱与避免方法
"""
def test_avoid_test_interdependence(self):
"""
陷阱1:测试间依赖
避免:每个测试应该独立,不依赖其他测试的执行顺序或结果
"""
# 好的做法:在setUp中初始化状态
counter = {'value': 0}
counter['value'] += 1
self.assertEqual(counter['value'], 1)
def test_avoid_external_dependencies(self):
"""
陷阱2:外部依赖未隔离
避免:使用Mock隔离外部依赖
"""
# 好的做法:Mock外部API
mock_api = Mock()
mock_api.get_user.return_value = {'id': 1, 'name': 'Test'}
result = mock_api.get_user(1)
self.assertEqual(result['name'], 'Test')
def test_avoid_order_dependence(self):
"""
陷阱3:测试顺序依赖
避免:每个测试应该可以单独运行
"""
# 不要假设测试按特定顺序执行
# unittest的执行顺序是方法名的字母顺序
pass
def test_avoid_hardcoded_data(self):
"""
陷阱4:硬编码数据
避免:使用工厂方法或fixtures生成测试数据
"""
# 好的做法:使用工厂方法
def create_test_user(name=None):
return {
'name': name or 'Test User',
'email': f'{name or "test"}@example.com'
}
user = create_test_user('Alice')
self.assertEqual(user['name'], 'Alice')
def test_avoid_insufficient_assertions(self):
"""
陷阱5:断言不充分
避免:验证所有重要的结果属性
"""
result = {'status': 'success', 'data': [1, 2, 3]}
# 好的做法:验证所有相关属性
self.assertEqual(result['status'], 'success')
self.assertEqual(len(result['data']), 3)
def test_avoid_missing_exception_tests(self):
"""
陷阱6:忽略异常处理测试
避免:测试正常和异常场景
"""
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# 测试正常场景
self.assertEqual(divide(10, 2), 5)
# 测试异常场景
with self.assertRaises(ValueError):
divide(10, 0)
def test_avoid_test_code_duplication(self):
"""
陷阱7:测试代码重复
避免:使用setUp和辅助方法
"""
# 好的做法:在setUp中创建共享对象
# 使用辅助方法封装重复逻辑
pass
if __name__ == '__main__':
unittest.main(verbosity=2)