第11章 单元测试

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹如年少遲夏歸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值