pytest之Monkeypatching(猴子补丁)

猴子补丁(monkey patching)理解

在运行时动态修改模块、类或函数,通常是添加功能或修正缺陷。
猴子补丁在代码运行时(内存中)发挥作用,不会修改源码,因此只对当前运行的程序实例有效。
因为猴子补丁破坏了封装,而且容易导致程序与补丁代码的实现细节紧密耦合,所以被视为临时的变通方案,不是集成代码的推荐方式。
在Python语言中,monkey patch 指的是对于一个类或者模块所进行的动态修改。在Python语言中,我们其实可以在运行时修改代码的行为。

monkeypatch使用场景

该monkeypatch固定装置提供了这些帮助程序方法,用于安全地修补和模拟测试中的功能:

monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=False)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)

考虑以下情形:

1.修改测试的函数行为或类的属性,例如,您不会为测试创建API调用或数据库连接,但是您知道预期的输出结果。用于使用monkeypatch.setattr()所需的测试行为来修补功能或属性。这可以包括您自己的功能。使用monkeypatch.delattr()删除用于测试的功能或属性。

2.修改字典的值,例如,您具有要针对某些测试用例进行修改的全局配置。使用monkeypatch.setitem()修补字典进行测试。monkeypatch.delitem()可用于删除项目。

3.修改测试的环境变量,例如在缺少环境变量的情况下测试程序的行为,或为已知变量设置多个值。 monkeypatch.setenv()并monkeypatch.delenv()可以用于这些补丁。

4. 在测试期间,用于修改和 更改当前工作目录的上下文。monkeypatch.setenv(“PATH”, value, prepend=os.pathsep)$PATHmonkeypatch.chdir()

5.使用monkeypatch.syspath_prepend()修改sys.path哪个也会调用pkg_resources.fixup_namespace_packages()和importlib.invalidate_caches()。

monkeypatch类的方法

monkeypatch夹具返回的对象记录了setattr / item / env / syspath的更改。

通过context() 返回一个新MonkeyPatch对象的上下文管理器,该对象with在退出时撤消在块内完成的所有修补操作:

import functools
def test_partial(monkeypatch):
    with monkeypatch.context() as m:
        m.setattr(functools, "partial", 3)

在需要在测试结束前撤消某些补丁的情况下很有用,例如stdlib模拟功能,如果被模拟可能会破坏pytest本身

一,设置属性

setattr(target,name,value = ,raising = True )
在目标上设置属性值,记住旧值。默认情况下,如果属性不存在,请引发AttributeError。
该raising值确定如果属性不存在(默认为True,这意味着它将raising )。

为方便起见,您可以指定一个字符串,target该字符串将被解释为点分导入路径,最后一部分是属性名称。
示例: 将设置模块的功能。monkeypatch.setattr(“os.getcwd”, lambda: “/”)getcwdos

delattr(target,name = ,raising = True )
name从中删除属性target,默认情况下,如果属性以前不存在,则引发AttributeError。
如果raising设置为False,则缺少该属性将不会引发异常。

如果未name指定no 并且target它是一个字符串,它将被解释为带点的导入路径,最后一部分是属性名称。

二,设置字典
setitem(dic,name,value )
将字典条目设置name为值。

delitem(dic,name,raising = True )
name从字典中删除。如果它不存在,请引发KeyError。
如果raising设置为False,则如果缺少密钥,则不会引发异常。

三,设置环境
setenv(name,value,prepend = None )
将环境变量设置name为value。如果prepend 是字符,则读取当前环境变量值,并在value相邻的prepend字符前加上该字符。

delenv(name,raising= True )
name从环境中删除。如果它不存在,请引发KeyError。
如果raising设置为False,则缺少环境变量时不会引发异常。

四,其他
syspath_prepend(路径)
前置path到sys.path导入位置列表。

chdir(路径)
将当前工作目录更改为指定的路径。路径可以是字符串或py.path.local对象。

undo()
撤消之前的更改。该调用消耗了撤消堆栈。除非您在撤消调用之后进行更多的猴子修补,否则再次调用它没有任何作用。
通常不需要调用undo(),因为在拆卸过程中会自动调用它。

monkeypatch构建模拟类替代请求接口返回的对象

monkeypatch.setattr()可以与类结合使用,以模拟从函数而不是值返回的对象。想象一个简单的函数来获取API URL并返回json响应。

import requests

def get_json(url):
    """请求一个接口,返回json对象"""
    r = requests.get(url)
    return r.json()

r为了测试目的,我们需要模拟返回的响应对象。模拟r需要一个.json()返回字典的方法。可以通过定义一个class来表示在我们的测试文件中r。

class MockResponse:
   '''定义一个mock类和方法,返回固定json对象'''
    @staticmethod
    def json():
        return {"mock_key": "mock_response"}


def test_get_json(monkeypatch):
    ''' 
    定义一个mock_get方法,无论传入什么参数,都返回上面的mock类,然后使用
    monkeypatch.setattr对requests.get重新设置,这样requests.get有了属性 
    “mock_get”,再次使用时就直接使用了mock_get方法
    '''
    def mock_get(*args, **kwargs):
        return MockResponse()
    monkeypatch.setattr(requests, "get", mock_get)
    result = get_json("https://fakeurl")
    assert result["mock_key"] == "mock_response_1"

执行用例成功:
在这里插入图片描述
monkeypatch将模拟requests.get与我们的mock_get函数一起应用。
该mock_get函数返回MockResponse该类的实例,该类具有json()定义为返回已知测试字典的方法,并且不需要任何外部API连接。可以MockResponse针对要测试的场景以适当的复杂度构建类。

使用fixture:

import pytest
import requests

class MockResponse:
    @staticmethod
    def json():
        return {"mock_key": "mock_response"}

# monkeypatch操作移到fixture中
@pytest.fixture
def mock_response(monkeypatch):
    """Requests.get() mock返回json值: {'mock_key':'mock_response'}."""
    def mock_get(*args, **kwargs):
        return MockResponse()
    monkeypatch.setattr(requests, "get", mock_get)


# 注意我们的测试函数传入的是fixture方法,而不是直接传入monkeypatch 
def test_get_json(mock_response):
    result = app.get_json("https://fakeurl")
    assert result["mock_key"] == "mock_response"

注意:
建议使用 MonkeyPatch.context()修补程序来限制要测试的块,
该对象with在退出时撤消在块内完成的所有修补操作。

import functools

def test_partial(monkeypatch):
    with monkeypatch.context() as m:
        m.setattr(functools, "partial", 3)
        assert functools.partial == 3

猴子补丁字典例子

DEFAULT_CONFIG = {"user": "user1", "database": "db1"}

def create_connection_string(config=None):
    config = config or DEFAULT_CONFIG
    return f"User Id={config['user']}; Location={config['database']};"
    def test_connection(monkeypatch):

    monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
    monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
    
    expected = "User Id=test_user; Location=test_db;"
    result = app.create_connection_string()
    assert result == expected

使用monkeypatch.delitem()删除值。

import pytest

def test_missing_user(monkeypatch):

    monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
    '''因为未传递配置引发KeyError,并且默认值现在缺少“user”项。'''
    with pytest.raises(KeyError):
        _ = app.create_connection_string()
pytest-bdd是一个用于在pytest框架下编写BDD(行为驱动开发)样式的测试的插件。它提供了一种结构化的方式来组织和编写测试。 在pytest-bdd中,我们可以使用"Examples"关键字来定义不同的测试场景和参数。它允许我们通过不同的参数值运行同一个场景,并且将这些参数值与期望结果相匹配。 举个例子,我们有一个场景是测试加法操作。我们可以使用"Examples"来定义不同的参数值,例如两个正数相加、两个负数相加、一个正数和一个负数相加等等。我们可以使用以下代码来实现: ``` Feature: Addition Scenario: Add two positive numbers Given I have two positive numbers "<a>" and "<b>" When I add the numbers together Then the result is "<result>" Examples: | a | b | result | | 2 | 3 | 5 | | 4 | 6 | 10 | | 8 | 9 | 17 | ``` 在这个例子中,我们定义了三个不同的参数组合,即(2, 3),(4, 6),(8, 9),并且分别定义了它们的期望结果为5,10和17。 当我们运行这个测试时,pytest会依次取出每个参数组合,并将其传递给相应的测试步骤中。在这个例子中,它会依次运行三次测试,每次比较实际结果和期望结果是否相等。 总结起来,pytest-bdd中的"Examples"用法允许我们使用不同的参数组合运行同一个场景,并且将这些参数与期望结果相匹配。这大大简化了测试编写的过程,并提高了测试的可读性和可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值