unittest 完全指南:从入门到生产实践
·
第1章 unittest基础架构与核心概念
1.1 unittest框架架构总览
unittest是Python标准库中的单元测试框架,其设计灵感来源于Java的JUnit。理解其架构是掌握unittest的第一步。
1.2 核心组件详解
1.2.1 TestCase - 测试用例基类
TestCase是unittest中最重要的类,所有测试类都必须继承它。
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用于组织和组合多个测试用例或测试套件。
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负责从各种来源加载测试。
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负责执行测试并输出结果。
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存储测试执行的结果信息。
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 测试执行流程详解
1.4 第一个完整的unittest示例
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 断言方法分类总览
2.2 相等性断言详解
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 布尔与身份断言详解
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 比较断言详解
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 异常断言详解
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 成员关系与类型断言详解
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 自定义断言方法
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 夹具生命周期详解
3.2 方法级别夹具:setUp/tearDown
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
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
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进行资源清理
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 上下文管理器与资源管理
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对象核心概念
4.2 Mock基础用法
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与特殊方法
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装饰器详解
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上下文管理器
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
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实战:数据库测试
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实现参数化
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的使用
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()方法
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 测试发现机制
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 测试加载策略
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 组织大型项目测试
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 测试结果处理
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测试报告
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 跳过测试装饰器
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 异步测试支持
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 代码覆盖率分析
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 测试最佳实践
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%
- 特点:测试单个函数或类的行为,隔离外部依赖
- 工具:unittest、pytest
- 优势:执行速度快,易于定位问题,维护成本低
- 示例:测试Calculator类的add方法
-
集成测试(中层)
- 比例:15-20%
- 特点:测试多个组件之间的交互
- 工具:unittest + Mock、pytest
- 优势:验证组件协作,发现集成问题
- 示例:测试UserRepository与数据库的交互
-
端到端测试(顶层)
- 比例:5-10%
- 特点:测试整个应用的完整流程
- 工具:Selenium、Cypress等
- 优势:验证用户场景,确保系统整体功能
- 示例:测试用户登录到完成购买的完整流程
测试金字塔的优势
- 平衡测试覆盖:确保关键功能有足够的测试覆盖
- 优化测试速度:大量快速的单元测试确保基本功能正常
- 降低维护成本:底层测试更容易维护和更新
- 快速反馈:单元测试失败可以快速定位问题
- 风险控制:上层测试捕获集成和系统级问题
实践建议
- 优先编写单元测试:确保核心功能的正确性
- 适度编写集成测试:验证组件间的交互
- 谨慎编写端到端测试:关注核心用户流程
- 保持测试金字塔比例:避免过多的端到端测试导致测试套件运行缓慢
- 持续评估测试策略:根据项目特点调整测试比例
第10章 生产环境最佳实践与实战
10.1 CI/CD集成
# .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 数据库测试最佳实践
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应用测试
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 常见测试陷阱与避免方法
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)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)