11 单元测试
11.1 测试的连续性
-
1.2 隔离环境
-
''' eg. 考虑根据一个人的结婚日期确定其年龄的函数。 从外部数据库获得关于此人信息(生日、结婚纪念日) 并计算日期交汇点,从而确定这个人的当前年龄 ''' def get_person_db(person_id): pass def calculate_age_at_wedding(person_id): # 数据库获取 person = get_person_db(person_id) anniversary = person['anniversary'] birthday = person['birthday'] age = anniversary.year- birthday.year # 如果生日比结婚纪念日晚 if birthday.replace(year=anniversary.year) > anniversary: age -= 1 return age
-
2.1代码布局
'''修改:按照容易测试的方式重构代码''' def calculate_age_at_wedding(person): anniversary = person['anniversary'] birthday = person['birthday'] age = anniversary.year- birthday.year if birthday.replace(year=anniversary.year) > anniversary: age -= 1 return age
-
2.2测试函数
from datetime import date def test_calculate_age_at_wedding(): person = {'anniversary':date(2012,4,21), 'birthday':date(1986,6,15)} age = calculate_age_at_wedding(person) assert age == 25, 'Expected age 25,got %d.'%age person = {'anniversary': date(1969, 4, 21), 'birthday': date(1945, 6, 15)} age = calculate_age_at_wedding(person) assert age == 24, 'Expected age 24,got %d.' % age
11.3 单元测试框架
-
unittest模块期望使用 unittest.TestCase子类能够找到测试组,每一个测试都必须是其名称以test开头的函数。调用 self.assert.Equal,unittest.TestCase类为assert 提供了大量的包装器,用于标准化错误消息以及提供一些样板
from datetime import date import unittest def calculate_age_at_wedding(person): anniversary = person['anniversary'] birthday = person['birthday'] age = anniversary.year- birthday.year if birthday.replace(year=anniversary.year) > anniversary: age -= 1 return age class Tests(unittest.TestCase): def test_calculate_age_at_wedding(self): person = {'anniversary': date(2012, 4, 21), 'birthday': date(1986, 6, 15)} age = calculate_age_at_wedding(person) self.assertEqual(age, 25) person = {'anniversary': date(1969, 4, 21), 'birthday': date(1945, 6, 15)} age = calculate_age_at_wedding(person) self.assertEqual(age, 24) def test_failure_case(self): person = {'anniversary': date(1969, 4, 21), 'birthday': date(1945, 6, 15)} age = calculate_age_at_wedding(person) self.assertEqual(age, 24) def test_error_case(self): person={} age = calculate_age_at_wedding(person) self.assertEqual(age, 24) # 跳过测试 @unittest.skipIf(True, 'this test was skipped.') def test_skipped_case(self): pass
-
python解释器提供一个标记 -m 用于接收一个来标准库或sys.path的模块,并把该模块作为脚本执行。
-
python -m unittest wedding
执行模块- wedding模块被载入
- unittest模块发现了一个unittest.TestCase子类
- 实例化该类并执行以单词 test开头的所有方法
- 对于成功执行的测试,unittest输出会打印一个句号字符(.)
- 若测试执行失败,则打印字母(F)
- 若发生错误,则输出字母(E)
- 遇到希望跳过的测试会打印字母(S)
11.3.2载入测试
- unittest.TestLoader 提供了从完整项目树中以编程的方式获取测试的扩展机制;如果使用默认测试载入类,则可以使用关键字discover 触发。
python -m unittest discover
- 默认情况下,他期望所有包含测试的文件命名遵循
test*.py
wedding.py
文件 测试代码 移动到 匹配模式(test_wedding.py
)测试系统会发现该文件
- 默认情况下,他期望所有包含测试的文件命名遵循
11.4 模拟
-
目的:显式拆分被测试的代码片段
-
模拟是在测试中声明特定函数调用给出一个特定输出的过程,而函数调用本身会被禁止
-
mock本质是 一个打补丁的库,它临时将给定命名空间的一个变量替换为一个名称为MagicMock的特殊对象,然后在模拟范围结束后将变量还原为之前的值。MagicMock对象基本接受对其的任何调用,并返回任何你让它返回的值
import unittest import sys from datetime import date try: from unittest import mock except ImportError: import mock def get_person_from_db(person_id): # 抛出异常 禁用函数 raise RuntimeError('The real `get_person_from_db`function was called') def calculate_age_at_wedding(person_id): # 数据库获取 person = get_person_from_db(person_id) anniversary = person['anniversary'] birthday = person['birthday'] age = anniversary.year - birthday.year # 如果生日比结婚纪念日晚 if birthday.replace(year=anniversary.year) > anniversary: age -= 1 return age class Tests(unittest.TestCase): def test_calculate_age_at_wedding(self): module = sys.modules[__name__] with mock.patch.object(module, 'get_person_from_db') as m: m.return_value = {'anniversary': date(2012, 4, 21), 'birthday': date(1986, 6, 15)} age = calculate_age_at_wedding(person_id=42) self.assertEqual(age, 25)
-
MagicMock.assert_called_once_with 该方法断言两件事:
MagicMock
被调用且只被调用一次;使用指定的参数签名''' person_id = 42 这里assert_called_once_with(42)的42是参数签名, 如果其内容不和person_id一致,则会出错。 assert_called_with类似 但被调用超过一次时并不会测试失败 ''' class Tests(unittest.TestCase): def test_calculate_age_at_wedding(self): module = sys.modules[__name__] with mock.patch.object(module, 'get_person_from_db') as m: m.return_value = {'anniversary': date(2012, 4, 21), 'birthday': date(1986, 6, 15)} age = calculate_age_at_wedding(person_id=85) self.assertEqual(age, 25) # assert that the `get_person_from_db` method was called the way we expect m.assert_called_once_with(42)
11.4.3 检查模拟
-
调用次数与状态
'''是否被调用''' from unittest import mock m = mock.MagicMock() m.called #False m(foo='bar') #<MagicMock name='mock()' id='2667496652360'> m.called #True '''调用确切次数''' m.call_count()
-
多次调用
MagicMock
对象提供assert_has_calls方法;mock库中提供call对象,每当发起一个对MagicMock
对象调用时,它都会在内部创建一个存储调用签名(并将其附加到对象内的mock_calls列表)的call对象。如果签名匹配,认为call对象相等。
from unittest.mock import call a = call(42) b = call(42) c = call('foo') a is b # False a == b # True a == c # False
from unittest.mock import call,MagicMock m = MagicMock() m.call('a') m.call('b') m.call('c') m.call('d') m.assert_has_calls([call.call('b'), call.call('c')])
-
检查调用
-
查看调用对象自身以及发送给它的参数;工作机制是call类,实际上是tuple的子类,并且调用对象是包含三个元素的元组,第二个和第三个参数是调用签名。
from unittest.mock import call c = call('foo', 'baz', sapm='eggs') print(c[0]) # print(c[1]) # ('foo', 'baz') print(c[2]) # {'sapm': 'eggs'} assert 'baz' in c[1] assert c[2]['spam'] == 'eggs'
-
11.5其他测试工具
-
coverage
# 该代码会创建 .coverage文件,该代码包含哪些代码被执行的信息 coverage run -m unittest mock_wedding # coverage report # -m 输入结果添加哪一行代码被跳过 coverage report -m
-
tox