有时,测试需要调用依赖于全局设置的功能,或调用不易测试的代码,如网络访问。

使用 monkeypatch 夹具可以安全地设置/删除属性、字典项或环境变量,或修改 sys.path 以进行导入。
请求的测试功能或夹具完成后,所有修改都将被撤消。

通过写了一个demo,对官方文档进行补充。

使用monkeypatch实现模拟类时,需直接替换目标文件中引入的工具类,而不是在目标文件中替换目标类中实例化后的工具类变量。

原函数

初始化了一个ssh工具类文件:ssh_tools.py,与调用了这个工具类的文件demo_module.py

ssh_tools.py

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# ------------------------------ #
# 原函数,被引用的ssh工具类的函数
# ------------------------------ #


class SshCom():
    def __init__(self,server_info) -> None:
        pass
    pass
demo_module.py

#! /usr/bin/env python3
# _*_ coding : UTF-8 _*_


# ------------------------------ #
# 调用了ssh工具的函数
# ------------------------------ #

from ssh_tools import SshCom

class LinuxInfo():
    def __init__(
        self, 
        server_info: dict
    ) -> None:
        self.ssh_com = SshCom(server_info)

    @property
    def get_card_drive_lsmod(self):
        card_drive_lsmod = self.ssh_com.ssh_get_response(
        "lsmod | grep -i nvidia"
        )
        if card_drive_lsmod:
            return True
        else:
            return False

monkeypatch实现模拟类

有两种实现方式,均可实现用monkeypatch替换原工具类函数,实现不连接网络也可以进行单元测试。
我这里框架使用的pytest,所以优先使用monkeypatch

test.pytest.py

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# ------------------------------ #
# 使用 pytest 单元测试函数
# 实现在不方便通过网络调试代码时,
# 使用monkeypatch替换,被测函数中,使用到网络的工具类
# 安全地设置/删除属性、字典项或环境变量,或进行修改sys.path以进行导入。
# ------------------------------ #


import pytest


def test_get_card_drive_lsmod_demo1(monkeypatch):
    """第一种方法替换需要被测试的函数文件中引入SshCom

    替换目标文件中被导入的类,替换为模拟类。

    :param object monkeypatch: 用于模拟环境的补丁模块
    :return bool: 通过或不能通过
    """
    class MockSshCom():
        def __init__(self,server_info) -> None:
            pass
        def ssh_get_response(self,command):
            if "nvidia" in command:
                return True
            else:
                return False
            
    import demo_module
    monkeypatch.setattr(demo_module,"SshCom",MockSshCom)

    linux_info = demo_module.LinuxInfo(server_info={})

    assert linux_info.get_card_drive_lsmod == True


def test_get_card_drive_lsmod_demo2(monkeypatch):
    """第二种方法替换需要被测试的函数文件中引入SshCom
    为了方便起见,可以指定一个字符串,该字符串将被解释为点分导入路径,最后一部分是属性名称:
    这里是替换为模拟类。

    :param object monkeypatch: 用于模拟环境的补丁模块
    :return bool: 通过或不能通过
    """
    class MockSshCom():
        def __init__(self,server_info) -> None:
            pass
        def ssh_get_response(self,command):
            if "nvidia" in command:
                return True
            else:
                return False
            
    from demo_module import LinuxInfo
    monkeypatch.setattr("demo_module.SshCom",MockSshCom)

    
    linux_info = LinuxInfo(server_info={})

    assert linux_info.get_card_drive_lsmod == True

unitest

也可以在pytest的用例中,使用uniitest.mock的方式,只不过不太推荐

test_unittest.py

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# ------------------------------ #
# pytest的测试用例。
# 使用unittest.mock实现monkeypatch功能
# 替换被测函数中,使用到网络的工具类
# 可以跑通
# ------------------------------ #

import demo_module



class LinuxInfo():
    def __init__(self,server_info:dict,ssh_com=None) -> None:
        self.ssh_com = ssh_com 
    
    @property
    def get_card_drive_lsmod(self):
        card_drive_lsmod = self.ssh_com.ssh_get_response("lsmod | grep -i nvidia")
        if card_drive_lsmod:
            return True
        else:
            return False
        
import unittest
from unittest import mock

class TestLinuxInfo(unittest.TestCase):
    def test_get_card_drive_lsmod(self):
        mock_ssh_com = mock.Mock()
        linux_info = LinuxInfo(server_info={},ssh_com = mock_ssh_com)

        mock_ssh_com.ssh_get_response.return_value = "nvidia"

        result = linux_info.get_card_drive_lsmod
        self.assertTrue(result)

        mock_ssh_com.ssh_get_response.return_value = ""
        result = linux_info.get_card_drive_lsmod
        self.assertFalse(result)

也可以用pytest的夹具,来调用unittest.mock

test_fixture.py

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# ------------------------------ #
# pytest的测试用例。
# 使用unittest.mock实现monkeypatch功能
# 替换被测函数中,使用到网络的工具类
# 在这里用到了夹具来引入unittest.mock
# ------------------------------ #

import pytest
from unittest import mock
from ssh_tools import SshCom

class LinuxInfo():
    def __init__(
        self, 
        server_info: dict,
        ssh_com = None
    ) -> None:
        self.ssh_com = ssh_com

    @property
    def get_card_drive_lsmod(self):
        card_drive_lsmod = self.ssh_com.ssh_get_response(
        "lsmod | grep -i nvidia"
        )
        if card_drive_lsmod:
            return True
        else:
            return False


@pytest.fixture
def mock_ssh_com():
    return mock.Mock()

def test_get_card_drive_lsmod(mock_ssh_com):
    linux_info = LinuxInfo(server_info={},ssh_com = mock_ssh_com)

    mock_ssh_com.ssh_get_response.return_value = "nvidia"
    result = linux_info.get_card_drive_lsmod
    assert result

    mock_ssh_com.ssh_get_response.return_value = ""
    result = linux_info.get_card_drive_lsmod
    assert not result

拓展

除了monkeypatch,unittest.mock以外,还有MagicMock等方法,这个就到后面有需求的时候再研究了。


参考:
[ 三年后的杂谈 ] 测试之Mock
How to monkeypatch/mock modules and environments

最后修改:2023 年 08 月 21 日
如果觉得我的文章对你有用,请随意赞赏