python mock 2:Mock类属性和断言的使用

一. Mock类说明

在进行测试时,可以通过 Mock 类对被测的对象、方法进行模拟(mock),进而完成测试任务。
大部分情况下,Mock 与 MagicMock 两个类可以相互替换,而 MagicMock 是一个更易用的类,通常情况下,使用它就可以了。在下面案例中,无论使用 Mock 还是 MagicMock 结果均相同。

官网:https://docs.python.org/3.11/library/unittest.mock.html#the-mock-class
原文:Mock is a flexible mock object intended to replace the use of stubs and test doubles throughout your code. Mocks are callable and create attributes as new mocks when you access them. Accessing the same attribute will always return the same mock. Mocks record how you use them, allowing you to make assertions about what your code has done to them.
翻译:> Mock是一种灵活的模拟对象,用于替换代码中存根(桩 stub)和测试替身(test doubles)的使用。mock是可调用的,并且在访问它们时将属性创建为新的mock。访问相同的属性将始终返回相同的mock。mock记录了您如何使用它们,允许您断言代码对它们做了什么。


二. Mock使用场景

2.1. 使用场景介绍

官网说明:https://docs.python.org/zh-cn/3.11/library/unittest.mock-examples.html

1). 模拟函数调用
2). 记录“模拟函数”被调用的情况
    (1). 通过属性:called、call_count、call_args …
    (2). 通过断言:assert_called_with() 、assert_called_once() …


源码: … \ python \ Lib \ unittest \ mock.py
1). 定义Mock类
2). 用于记录模拟函数被调用情况的方法(called、call_count、call_args …)

class Mock(CallableMixin, NonCallableMock): .....
class NonCallableMock(Base):
    called = _delegating_property('called')
    call_count = _delegating_property('call_count')
    call_args = _delegating_property('call_args')
    call_args_list = _delegating_property('call_args_list')
    mock_calls = _delegating_property('mock_calls')
    .........

通过PyCharm,可以看到可用的断言:
在这里插入图片描述


2.2. 属性说明


以下属性具体使用见后面的案例。

  • called:记录被模拟的方法是否被调用过,调用过则返回 true
  • call_count:记录被模拟的方法被调用过的次数,返回整数调用次数
  • add.call_args:记录被模拟的方法在最后一次调用时的情形,包括使用的参数
  • call_args_list:记录被模拟的方法自己本身被调用的所有历史记录,返回一个列表,记录方法调用链。
  • mock_calls:记录被模拟的方法自己本身及其子方法被调用的所有历史记录,返回一个列表,记录方法调用链。

2.3. 断言说明


以下断言,如果断言通过,则返回 None,不会产生输出;如果断言失败,则抛出 AssertionError 异常并打印异常信息。具体使用见后面的案例

  • assert_called:断言被模拟的方法是否至少被调用过1次
  • assert_called_with:断言模拟方法是否最近一次被以某种参数方式调用过
    • 如:obj.add.assert_called_with(1,2),表示:为 obj 对象模拟的 add 方法是否以参数 (1,2) 的形式被最后调用过
  • assert_called_once:断言模拟方法是否被调用过,且仅被调用了1次
  • assert_called_once_with:断言模拟方法是否最后一次以某种参数方式被调用过,且仅被调用了1次
    • 如:obj.add.assert_called_once_with(1, 2),表示:为 obj 对象模拟的 add 方法是否以参数 (1,2) 的形式被最后调用过,且仅被调用了1次
  • assert_any_call:断言被模拟的方法是否以指定的参数被调用过。不同于 assert_called_with 和 assert_called_once_with,不用考虑是否为最后一次调用
  • assert_has_calls:断言被模拟的方法被多次调用的序列是否符合预期(见案例2)



三. 案例

3.1. 案例1


from unittest.mock import Mock, MagicMock


class TmpClass:
    pass


if __name__ == '__main__':
    r = TmpClass()
    # 为该对象添加一个模拟的add方法,返回值为3
    r.add = Mock(return_value=3)
    # r.add = MagicMock(return_value=3)  结果相同

    # 进行多次模拟方法调用
    print(f"第一次调用模拟方法(add):{r.add},返回值:{r.add(1, 2)}")
    print(f"第二次调用模拟方法(add):{r.add},返回值:{r.add(2, 3, 4)}")
    print(f"调用模拟子方法(add.sub)(未设定返回值):{r.add.sub(5, 7)}")

    # 打印相关属性(用于记录被模拟方法的执行结果)
    print(f"\n被模拟的add方法,输出其内置属性:")
    print(f"  -- 1. 是否被调用过:{r.add.called}")
    print(f"  -- 2. 被调用的次数:{r.add.call_count}")
    print(f"  -- 3. 最后一次被调用为:{r.add.call_args}")
    print(f"  -- 4. 仅自己被调用的记录:{r.add.call_args_list}")
    print(f"  -- 5. 所有(包括子方法)被调用的记录:{r.add.mock_calls}")

    # 打印相关断言(用于判断被模拟方法的执行情形)

    print(f"\n被模拟的add方法,调用后进行断言:")
    try:
        print(f"  -- 断言1通过 assert_called_with(2,3,4):是否最近一次以(2,3,4)为参数被调用过("
              f"{r.add.assert_called_with(2, 3, 4) is None})")
    except AssertionError as ae1:
        print(f"  -- 断言1失败 assert_called_with(2,3,4):因为最后一次调用时没有使用该参数")

    try:
        print(f"  -- 断言2通过 assert_called_with(1,2):是否最近一次,以(1,2)为参数被调用过:"
              f"{r.add.assert_called_with(1,2) is None}")
    except AssertionError as ae1:
        print(f"  -- 断言2失败 assert_called_with(1,2):因为虽然以该参数被调用过,但不是最近一次调用")

    try:
        print(f"  -- 断言3通过:是否仅被调用过1次:"
              f"{r.add.assert_called_once() is None}")
    except AssertionError as ae2:
        print(f"  -- 断言3失败 assert_called_once:因为方法被调用不止1次")

    try:
        print(f"  -- 断言4通过:是否仅被调用过1次,且以(1,2)为参数:"
              f"{r.add.assert_called_once_with(1, 2) is None}")
    except AssertionError as ae3:
        print(f"  -- 断言4失败 assert_called_once_with(1,2):因为虽然以该参数被调用过,且只被调用1次,但不是最近一次调用")

    try:
        print(f"  -- 断言5通过 add.assert_any_call(1, 2):是否被调用过次,且以(1,2)为参数("
              f"{r.add.assert_any_call(1, 2) is None})")
    except AssertionError as ae3:
        print(f"  -- 断言4失败 add.assert_any_call(1, 2):因为以该参数未被调用过")


执行后输出结果:


第一次调用模拟方法(add):<Mock id='2119543070544'>,返回值:3
第二次调用模拟方法(add):<Mock id='2119543070544'>,返回值:3
调用模拟子方法(add.sub)(未设定返回值)<Mock name='mock.sub()' id='2119550955600'>

被模拟的add方法,输出其内置属性:
  -- 1. 是否被调用过:True
  -- 2. 被调用的次数:2
  -- 3. 最后一次被调用为:call(2, 3, 4)
  -- 4. 仅自己被调用的记录:[call(1, 2), call(2, 3, 4)]
  -- 5. 所有(包括子方法)被调用的记录:[call(1, 2), call(2, 3, 4), call.sub(5, 7)]

被模拟的add方法,调用后进行断言:
  -- 断言1通过 assert_called_with(2,3,4):是否最近一次以(2,3,4)为参数被调用过(True-- 断言2失败 assert_called_with(1,2):因为虽然以该参数被调用过,但不是最近一次调用
  -- 断言3失败 assert_called_once:因为方法被调用不止1-- 断言4失败 assert_called_once_with(1,2):因为虽然以该参数被调用过,且只被调用1次,但不是最近一次调用
  -- 断言5通过 add.assert_any_call(1, 2):是否被调用过次,且以(1,2)为参数(True

3.2. 案例2:assert_has_calls 断言


当被模拟的方法被多次调用,形成了一个调用链(如:方法1 -> 方法2 ->方法3)时,可以对调用的顺序进行断言。

  • 通过 call 对象创建一个预期的调用链(列表)
  • 断言预期的调用链是否存在于模拟方法的调用链中
    • 不用考虑其出现的位置和顺序
from unittest.mock import Mock, MagicMock, call

if __name__ == '__main__':
    m = Mock()
    # m = MagicMock()

    # 模拟链式方法调用
    m().foo().bar().baz()
    # 再次模拟链式方法调用
    m.one().two().three()
    print(m.mock_calls)

    # 创建一个预期调用链 - 使符合预期
    calls1 = call.one().two().three().call_list()
    print(f"\n预期调用链为:{calls1}")
    try:
        print(f"断言 assert_has_calls 通过:调用链符合预期:"
              f"{m.assert_has_calls(calls1) is None}")
    except AssertionError as ae3:
        print(f"断言 assert_has_calls 失败:不符合预期调用链")

    # 创建一个预期调用链 - 使不符合预期
    calls2 = call.one().two().temp().call_list()
    print(f"\n预期调用链为:{calls2}")
    try:
        print(f"断言 assert_has_calls 通过:调用链符合预期:"
              f"{m.assert_has_calls(calls2) is None}")
    except AssertionError as ae3:
        print(f"断言 assert_has_calls 失败不符合预期调用链")

输出:

[call(),  call().foo(),   call().foo().bar(),    call().foo().bar().baz(),
 call.one(),   call.one().two(),   call.one().two().three()]

预期调用链为:[call.one(), call.one().two(), call.one().two().three()]
断言 assert_has_calls 通过:调用链符合预期:True

预期调用链为:[call.one(), call.one().two(), call.one().two().temp()]
断言 assert_has_calls 失败不符合预期调用链
  • 第二次断言失败,因为预期调用序列上有一个 temp() 方法,但其不在模拟的调用序列中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值