Mock 模块使用说明


功能介绍

好的编码习惯都应该为每一行代码做覆盖测试,但有些时候代码处理的是从网络上获取的内容,或者设备的返回,比如获取交换机路由器的运行结果,或者从网络上获取页面等等。这些动作要么需要联网,要么需要设备,但实际上我们只是想测试代码正确性而已,注重的是对返回的内容的处理而不必非要有实际设备。

mock 模块用于在单元测试中模拟其它代码的结果,比如某个函数需要调用其他函数,这个时候我们可以模拟这个第三方函数的结果来略过实际调用它,不光可以节省时间,也可以避免因为第三方函数出错而影响自己的代码,甚至可以很轻松的模拟难以出现的各种情况。

也正是因为这个模块是如此好用,在 Python2 中还需要单独安装 mock 模块,而 Python3.3 开始这个模块就被放入标准模块了,名叫 unittest.mock

使用思路和实例

在概念上, mock 用于模拟函数的返回,比如你有一个函数调用了另一个函数,而另一个函数的代码本身不是你写的,或者不需要在当前单元测试中测试,你只是希望拿到另一个函数返回的结果,这个时候就可以用 mock 来模拟那个函数来略过各种中间过程而直接得到结果。比如下面这样的代码结构:

#!/usr/bin/env python3
import re

def send_shell_cmd():
    return "Response from send_shell_cmd function"

def check_cmd_response():
    response = send_shell_cmd()
    print("response: {}".format(response))
    return re.search(r"mock_send_shell_cmd", response)

然后是单元测试,注意 patch 的用法,它是一个装饰器,需要把你想模拟的函数写在里面,然后在后面的单元测试案例中为它赋一个具体实例,再用 return_value 来指定模拟的这个函数希望返回的结果就可以了,后面就是正常单元测试代码。

@mock.patch("function_C")
@mock.patch("function_B")
@mock.patch("function_A")
def test_check_cmd_response(self, mock_function_A, mock_function_B, mock_function_C):
    mock_function_A.return_value = "Function A return"
    mock_function_B.return_value = "Function B return"
    mock_function_C.return_value = "Function C return"

    self.assertTrue(re.search("A", mock_function_A()))
    self.assertTrue(re.search("B", mock_function_B()))
    self.assertTrue(re.search("C", mock_function_C()))

如果函数是在其它文件中实现的,那么 mock 的方式又有不同:

# linux_tool.py
import re

class LinuxTool(object):
    def __init__(self):
        pass

    def send_shell_cmd(self):
        return "Response from send_shell_cmd function"

    def check_cmd_response(self):
        response = self.send_shell_cmd()
        print("response: {}".format(response))
        return re.search(r"mock_send_shell_cmd", response)

再来写单元测试的案例:

class TestSetupServer(TestCase):
    @mock.patch.object(linux_tool, "send_shell_cmd")
    def test_start_ftp_service_for_default_conf(self, mock_send_shell_cmd):
        mock_send_shell_cmd.side_effect = [
            "cmd1_response",
            "cmd2_response",
            "cmd3_response",
            "cmd4_response",
            "cmd5_response",
        ]

        self.mytool.start_ftp_service()

如果某个命令在循环中被调用,满足判断结果才会跳出循环,那么也要用 side_effect 来模拟循环中的每次结果,一定数清楚具体的循环次数或者精心设计返回,否则执行会出错。

模拟异常

用上面模拟同一个函数多次被调用的实例为例,如果希望主动引发异常,比如 Exception 那么可以这样:

class pexpect(object):
    """Fake pexpect class"""
    def __init__(self):
        """INIT"""
        self.before = None

    def spawnu(self):
        """Fake method"""
        pass


class UnitTest(unittest.TestCase):
    @mock.PropertyMock(pexpect, "before")
    @mock.patch.object(pexpect, "spawnu")
    def test_send_cli_cmd(self, mock_spawnu, mock_before):
        pass

MagicMock

mock.MagicMock 是 mock.Mock 的子类,区别就是 MagicMock 预置了其它 MagicMethod ,所谓 MagicMethod 在 Python 中表现为双下划线包围的方法,比如最熟悉的 init 或者 str 之类的。 mock.Mock 默认没有实现这些方法,如果想测试这些方法的行为就得自己写,而 MagicMock 默认预置了这些行为,这样像自增自减,列表的循环,计算符号的重载等 MagicMethod 就在 MagicMock 中内置了,如果不考虑这些那么 MagicMock 和 Mock 行为是一样的

一般情况下模拟都用 MagicMock ,因为这个模拟出来的行为更类似于我们预期

精准模拟第三方函数

自己写的模块大多数时候都需要调用其它函数 (比如大多数模块都会用的 os 或者 sys 模块) ,如何模拟这些第三方函数呢?可以看一个例子:

mock_send_shell_cmd.called
>> True

call_count

返回模拟的函数或方法被调用了几次:

>>> mock_send_shell_cmd.some_method3(cmd="ls -l", mode="shell")
>>> mock_send_shell_cmd.some_method3.call_args
call(cmd="ls -l", mode="shell")

还有一个叫 call_args_list ,这个用于 mock 的方法被多次调用的情况,会返回一个列表,列表中是每次被调用时的参数

assert_called_with

有时候我们不光想确认自己 mock 的东西有没有被调用,还想确认调用时传入的参数是不是正确的,就可以用 assert_called_with ,比如:

import os

# 面向对象开发中,往往需要载入其它模块,这个 ExternalClass 就用于模拟其它开发人员写的模块,我们既不知道它怎么做,也不知道做的对不对,只想模拟调用这个方法之后的结果
class ExternalClass(object):
    def __init__(self):
        self.external_attrib_a = None
        self.external_attrib_b = None

    def external_method_a(self):
        pass

class MyClass(object):
    def __init__(self):
        self.external = ExternalClass()

        self.attrib_a = None
        self.attrib_b = None

    def method_a(self):
        return self.external.external_method_a()

过程风格的基本代码

Mock 类中的属性

这个例子中准备测试 MyClass 中的 method_a 方法, method_a 则实例化 ExternalClass 类,并调用它的 external_method_a 方法。

我们不在乎 external_method_a 怎么干的,就想模拟它的返回值。这就要用到 mock.PropertyMock 方法

# 这是写文件的代码
def operate_file(file_name, content):
    with open(os.path.expanduser(filename), "wb") as fid:
        fid.write(content)

# 这里是测试代码
# mock.patch 用于模拟系统的 open 方法
from unittest import mock
@mock.patch("builtins.open", read_data="data")
def test_operate_file(mock_open):
    mock_open.read.return_value = True
    mock_open.write.return_value = True

# 上面是在 Python2 有效的代码,在 Python3 中 mock 建立了一个 mock_open 方法用来直接模拟,不需要用装饰器了,直接在函数内部这么写
def test_operate_file():
    with mock.patch("builtins.open", mock.mock_open(read_data=conf_lines)) as mock_open:
        ......

但是如果操作文件是在类里面,而且直接 open 文件以后用 for 循环文件句柄,没有 read, write 动作应该怎么做呢?看下面的例子:

# 函数内部直接模拟
import os
from unittest import mock

def function():
    mock_func = mock.patch("os.path.isfile", return_value=True)


# 使用装饰器
@mock.patch("os.path.isfile")
def function(mock_os_isfile):
    mock_os_isfile.return_value = True


# 使用 with 语句
def function():
    with mock.patch("os.path.isfile") as mock_os_isfile:
        mock_os_isfile.return_value = True

如果同时模拟多个模块或方法,那么多个 mock 之间用斜杠分隔,就像这样:

Class Device(object):
    def __init__(self):
        pass

    def login(self):
        pass


Class OperateDevice(object):
    def __init__(self):
        self.ins = Device()

    def login_device(self):
        self.ins.login()


Class TestOperateDevice(TestCase):
    def __init__(self):
        self.ins_operate_device = OperateDevice()

    def test_login_device(self):
        dev_obj = mock.Mock()
        dev_obj.login_device = mock.Mock()
        dev_obj.login_device.return_value = True

        self.ins_operate_device.login_device()

上面的例子测试的是 OperateDevice ,在里面的实例是类 Device,反正不管怎样,我们要模拟 login_device ,那么直接用 mock.Mock() 模拟一个类实例,然后再模拟一个方法并设置方法的值即可




原文链接:https://www.jianshu.com/p/55e5a6863c3f