Python单元测试——mock 模块

1. mock 简述

mock 模块是 Python 的测试框架 unittest 下的一个子包,是单元测试的一个重要模块。

单词 mock 有模拟的意思。在 Python 中,mock 可以理解为模拟一个方法、一个对象或者一个类等等,然后使用模拟的对象替换系统的一部分,对系统的一个单元进行测试,并对它们已使用的方式进行断言。

那为什么要使用 mock 呢?

因为,在实际生产中的项目非常复杂,对其进行单元测试的时候,会遇到以下问题:

  • 接口的依赖
  • 外部接口调用
  • 测试环境非常复杂

单元测试应该只针对当前单元进行测试,所有的内部或外部的依赖应该是稳定的。使用 mock 就可以对外部依赖组件实现进行模拟和替换,从而使得单元测试将焦点只放在当前的单元功能。

场景模拟:比如有 A 和 B 两个模块,A 模块中有调用到 B 模块的方法,但是现在,B 模块中被 A 模块调用的方法需要修改,而我们又不想让它影响 A 模块的功能测试,所以就用到了单元测试模块 unittest 中的 mock 模块,mock 模块就是模拟出一个假的 B 模块供 A 模块测试用。

2. mock 模块简介

mock 对象的属性如下:

from unittest.mock import MagicMock

obj = MagicMock()
print(dir(obj))
['assert_any_call', 'assert_called', 'assert_called_once', 'assert_called_once_with', 
'assert_called_with', 'assert_has_calls', 'assert_not_called', 'attach_mock', 
'call_args', 'call_args_list', 'call_count', 'called', 'configure_mock', 'method_calls', 
'mock_add_spec', 'mock_calls', 'reset_mock', 'return_value', 'side_effect']

mock 对象的属性可以分为四类:

  • 构造器;
  • 断言方法;
  • 管理方法;
  • 静态方法。

具体如下图所示。
在这里插入图片描述

2.1 构造器

mock 的构造器有四个参数:namespecreturn_valueside_effect

name 参数

namemock 对象的名称,是 mock 对象的唯一标识。

示例:

from unittest.mock import Mock

mock_obj = Mock(name='ok')
print(mock_obj)
print(repr(mock_obj))

输出:

<Mock name='ok' id='140435544582752'>
<Mock name='ok' id='140435544582752'>

可以看到,name 标识了唯一的一个 mock(打印的时候,后边会显示 ID)。其中,repr(object) 方法返回对象的字符串形式。

spec 参数

spec 设置的是 mock 对象的属性,可以是 property 或者方法,也可以是其他的列表字符串或者其他的 python 类。

spec 指定的是属性组成的 list:

from unittest.mock import Mock

list = ['kobe', 'curry']
mock_foo = Mock(spec=list)

print(mock_foo)
print(mock_foo.kobe)
print(mock_foo.curry)

输出:

<Mock id='139824239839840'>
<Mock name='mock.kobe' id='139824173697392'>
<Mock name='mock.curry' id='139824173697776'>

如果我们打印一个不存在的属性,如下

print(mock_foo.currycc)

则会报一个属性不存在的错误,如下

AttributeError: Mock object has no attribute 'currycc'

spec 指定的是一个类属性:

如下,创建一个 People 类,有 3 个属性(一个变量和两个方法)。

from unittest.mock import Mock

class People:
    _num = 24

    def f1(self):
        print('f1()')

    def f2(self, value):
        print('value = ', value)

mock_foo = Mock(spec=People)

print(mock_foo)
print(mock_foo._num)
print(mock_foo.f1())
print(mock_foo.f2())

将 spec 设置为一个 People 对象,这个 mock 对象就有三个属性了。输出如下:

<Mock spec='People' id='140560969070672'>
<Mock name='mock._num' id='140560969127056'>
<Mock name='mock.f1()' id='140560969129888'>
<Mock name='mock.f2()' id='140560969238224'>

return_value

return_value 设置的是 mock 对象被调用时的返回值。当这个 mock 对象被调用的时候,显示出的结果就是 return_value 的值。

方式一:使用 return_value 指定一个值

from unittest.mock import Mock

mock_foo = Mock(return_value = 24)
print(mock_foo)
mock_obj = mock_foo()
print(mock_obj)

输出:

<Mock id='140403200965216'>
24

当我们调用 mock 对象的时候,显示的就是 return_value 的值(也就是说 mock_obj 是带有一定的功能的)。

方式二:使用 return_value 指定一个对象

from unittest.mock import Mock

class People:
    _num = 24

    def f1(self):
        print('f1()')

    def f2(self, value):
        print('value = ', value)

p = People()
# 打印对象p的地址
print(p)

# 设置return_value为对象p
mock_p = Mock(return_value=p)
print(mock_p)

# 调用mock对象,会返回上面设置的对象p
mock_obj = mock_p()
print(mock_obj)

print('================')

# 调用mock的对象p的属性和方法
print(mock_obj._num)
print(mock_obj.f1())
# 调用方法也需要传入参数
print(mock_obj.f2(8))
<__main__.People object at 0x7f7bb4707e20>
<Mock id='140169218681200'>
<__main__.People object at 0x7f7bb4707e20>
================
24
f1()
None
value =  8
None

side_effect

side_effect 是和 return_value 是相反的,它会覆盖 return_value 的值。也就是说当这个 mock 对象被调用的时候,返回的是 side_effect 的值,而不是 return_value 的值。

side_effect 指定一个 list 或者 tuple 作为参数

from unittest.mock import Mock

list = [1, 2, 3]
mock_foo = Mock(return_value=100, side_effect=list)
mock_obj = mock_foo()
print(mock_obj)

mock_obj = mock_foo()
print(mock_obj)

mock_obj = mock_foo()
print(mock_obj)

mock_obj = mock_foo()
print(mock_obj)

输出:

Traceback (most recent call last):
  File "/home/10307952@zte.intra/PyProjects/py_note/my_note/mock_test/mock_init/init_side_effect.py", line 12, in <module>
    mock_obj = mock_foo()
  File "/usr/local/python38/lib/python3.8/unittest/mock.py", line 1081, in __call__
    return self._mock_call(*args, **kwargs)
  File "/usr/local/python38/lib/python3.8/unittest/mock.py", line 1085, in _mock_call
    return self._execute_mock_call(*args, **kwargs)
  File "/usr/local/python38/lib/python3.8/unittest/mock.py", line 1142, in _execute_mock_call
    result = next(effect)
StopIteration
1
2
3

注:

side_effect 指定一个 list 或者 tuple 作为参数后,会返回一个迭代器,来替换 return_value 的值。每执行一次,会返回 list 中的一个值,返回完的时候就会抛出异常。(类似于 Python 的生成器,每次执行 next 操作,都会返回一个值,当返回完的时候,就会抛出异常)

2.2 断言方法

assert_called_with(*argSeq, **argKW):检查 mock 方法是否获得了正确的参数。

当至少有一个参数有错误的值或者类型时,当参数的个数出错时,当参数的顺序不正确时,这个断言就会发生错误。

如下,方法 f1() 不需要参数,使用 mock 对象调用 f1() 之后,使用 assert_called_with() 断言成功。

from unittest.mock import Mock

class People:
    _num = 24

    def f1(self):
        print('f1()')

    def f2(self, value):
        print('value = ', value)

mock_foo = Mock(spec=People)

print(mock_foo)
mock_foo.f1()
mock_foo.f1.assert_called_with()

方法 f2() 需要一个参数,如果我们使用 mock 对象调用 f1() 时不传入参数,使用 assert_called_with() 则会断言失败。如下

mock_foo.f2()
mock_foo.f2.assert_called_with(10)

错误信息如下:

AssertionError: expected call not found.
Expected: f1()
Actual: f1(1)

assert_called_once_with(*argSeq, **argKW):当某个方法被多次调用的时候,它就会报错。

在上面的基础上,我们再调用一次方法 f2(),然后使用 assert_called_once_with(10) 断言,如下

mock_foo.f2(10)
mock_foo.f2.assert_called_once_with(10)

方法 f2() 被调用了两次,断言信息如下:

AssertionError: Expected 'f2' to be called once. Called 2 times.
Calls: [call(), call(10)].

assert_any_call():用于检查测试的 mock 对象在测试例程中是否调用了方法。

示例:

# 先用mock对象调用两次f2(),第一次不传参,第二次传参数10
mock_foo.f2()
mock_foo.f2(10)

# 断言mock对象是否调用了不带参数的f2()方法,断言成功
mock_foo.f2.assert_any_call()

# 断言mock对象是否调用了参数为200的f2()方法,断言失败
mock_foo.f2.assert_any_call(200)

断言失败信息如下:

AssertionError: f2(200) call not found

assert_has_calls():检查是否按照正确的顺序和正确的参数调用方法。所以,我们需要给出一个方法的调用顺序,assert 的时候按照这个顺序进行检查。

示例:

# 调用方法
mock_foo.f1()
mock_foo.f2()
mock_foo.f2(10)

# 按照上面的调用顺序进行断言,断言成功
foo_calls = [call.f1(), call.f2(), call.f2(10)]
mock_foo.assert_has_calls(foo_calls)

更换 foo_calls 指定的调用顺序,则断言失败,如下:

foo_calls = [call.f1(), call.f2(10), call.f2()]
mock_foo.assert_has_calls(foo_calls)

# 断言失败信息如下
AssertionError: Calls not found.
Expected: [call.f1(), call.f2(10), call.f2()]
Actual: [call.f1(), call.f2(), call.f2(10)]

参考:Python中的模块学习之mock模块

3. mock 示例

先定义一个类,有两个成员方法(一个有参数,一个无参数),还有一个静态方法。如下:

class Person:
    def __init__(self):
        self.__age = 10

    def get_fullname(self, first_name, last_name):
        return first_name + ' ' + last_name

    def get_age(self):
        return self.__age

    @staticmethod
    def get_class_name():
        return Person.__name__

3.1 使用 Mock 类,返回固定值

使用 return_value 指定返回值。

import unittest
from unittest import TestCase
from unittest.mock import Mock

from my_note.mock_test.mock_example.person import Person


class PersonTest(TestCase):
    def test_get_age(self):
        p = Person()

        # 不使用mock,正常调用get_age()
        self.assertEqual(p.get_age(), 10)

        # mock掉get_age(),让它返回20,使用return_value指定返回值
        p.get_age = Mock(return_value=20)
        self.assertEqual(p.get_age(), 20)

	def test_get_full_name(self):
        p = Person()

        # 正常调用get_fullname()
        print(p.get_fullname('kobe', 'bryant'))

        # mock掉get_fullname(),
        p.get_fullname = Mock(return_value='stephen curry')
        self.assertEqual(p.get_fullname(), 'stephen curry')
        
if __name__ == '__main__':
    unittest.main()

需要注意的是,上面的示例在调用 p.get_fullname 时没有给任何的参数,但是依然可以工作。并且,任意指定参数对结果并没有影响。如下:

self.assertEqual(p.get_full_name('1'), 'stephen curry')
self.assertEqual(p.get_full_name('1', '2'), 'stephen curry')
self.assertEqual(p.get_full_name('1', '2', '3'), 'stephen curry')

也就是说这种方式无法校验参数,如果想校验参数需要用 create_autospec 模块方法替代 Mock 类。

3.2 校验参数个数,再返回固定值

    def test_get_full_name2(self):
        p = Person()

        # 任意给定两个参数,依然会返回mock的值,但参数个数必须对
        p.get_full_name = create_autospec(p.get_full_name, return_value='stephen curry')
        self.assertEqual(p.get_full_name('1', '2'), 'stephen curry')

        # 如果参数个数不对,会报错TypeError: missing a required argument: 'first_name'
        self.assertEqual(p.get_full_name(), 'stephen curry')

3.3 使用 side_effect,依次返回指定值

可以指定一组返回值,然后使用 side_effect 依次返回。

    def test_get_age2(self):
        p = Person()

        # 指定一组返回值,side_effect会覆盖return_value的值
        list = [2, 4, 6]
        p.get_age = Mock(return_value=20, side_effect=list)

        self.assertEqual(p.get_age(), 2)
        self.assertEqual(p.get_age(), 4)
        self.assertEqual(p.get_age(), 6)

3.4 根据不同的参数,返回不同的值

    def test_get_full_name3(self):
        p = Person()

        values = {('kobe', 'bryant'): 'kobe bryant', ('stephen', 'curry'): 'stephen curry'}
        p.get_full_name = Mock(side_effect=lambda x, y: values[(x, y)])
        self.assertEqual(p.get_full_name('stephen', 'curry'), 'stephen curry')
        self.assertEqual(p.get_full_name('kobe', 'bryant'), 'kobe bryant')

3.5 抛出异常

    def test_raise_exception(self):
        p = Person()

        p.get_age = Mock(side_effect=TypeError('integer type'))
        # 只要调用get_age()方法,就会抛出异常
        self.assertRaises(TypeError, p.get_age())

这里调用了 p.get_age(),抛出异常如下:

TypeError: integer type
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值