小门板儿

Menu

pytest之猴子补丁(MonkeyPatch)/模拟(Mock)模块和环境

有时测试需要调用依赖于全局设置的功能,或者调用不容易测试的代码(如网络访问)。这个 monkeypatch fixture帮助您安全地设置/删除属性、字典项或环境变量,或修改 sys.path 用于导入。猴子补丁在代码运行时(内存中)发挥作用,不会修改源码,因此只对当前运行的程序实例有效

这个 monkeypatch fixture为测试中的安全修补和模拟功能提供了以下帮助方法:

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)

所有修改将在请求的测试功能或夹具完成后撤消。这个 raising 参数确定 KeyErrorAttributeError 如果设置/删除操作的目标不存在,则将引发

考虑以下情形:

1、为测试修改函数的行为或类的属性,例如有一个API调用或数据库连接,您将无法进行测试,但您知道预期的输出应该是什么。使用 monkeypatch.setattr 使用所需的测试行为修补函数或属性。这可以包括您自己的功能。使用 monkeypatch.delattr 删除测试的函数或属性。

2、修改字典的值,例如,对于某些测试用例,您需要修改全局配置。使用 monkeypatch.setitem 为测试修补字典。 monkeypatch.delitem 可用于删除项目。

3、修改测试的环境变量,例如在缺少环境变量时测试程序行为,或将多个值设置为已知变量。 monkeypatch.setenvmonkeypatch.delenv 可用于这些补丁。

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

5、使用 monkeypatch.syspath_prepend 修改 sys.path 它也会调用 pkg_resources.fixup_namespace_packagesimportlib.invalidate_caches() .

一、示例:monkeypatching函数

monkeypatch.setattr 用于修补 Path.home 使已知的测试路径 Path("/abc") 总是在运行测试时使用。这将删除出于测试目的对正在运行的用户的任何依赖。 monkeypatch.setattr 必须在调用将使用修补函数的函数之前调用。测试功能完成后, Path.home 修改将被撤消。

from pathlib import Path
​
def getssh():
    return Path.home() / ".ssh"
​
def test_getssh(monkeypatch):
    
    def mockreturn():
        return Path("/abc")
​
    monkeypatch.setattr(Path, "home", mockreturn)
​
    x = getssh()
    assert x == Path("/abc/.ssh")
输出:
a/test_mock.py::test_getssh PASSED

二、MonkeyPatching返回的对象:构建模拟类

monkeypatch.setattr 可以用来与类结合之后从函数中返回一个对象替代原有的值

#content of app.py
import requests
​
def get_json(url):
    r=requests.get(url)
    return r.json()
#content of test_mock.py
import requests
from c.app import get_json
class MockResponse:
    # mock json() 方法总是return一个指定的值
    @staticmethod
    def json():
        return {"mock_key": "mock_response"}
​
def test_get_json(monkeypatch):
    def mock_get(*arg,**kwargs):
        return MockResponse()
​
    monkeypatch.setattr(requests,'get',mock_get)
    result=get_json('http://www.baidu.com')
    assert result["mock_key"] == "mock_response"

monkeypatch 将模拟应用于 requests.get 与我们的 mock_get 功能。这个 mock_get 函数返回 MockResponse 类,其中有一个 json() 方法定义为返回已知的测试字典,不需要任何外部API连接。

可以使用MockResponse 为正在测试的场景使用适当的复杂性来初始化,例如,我们可以设置一个永远返回true的ok属性或是根据不同的输入字符串模拟json()方法的不同返回值。

在夹具中进行mock可以被多个测试共享:

#content of conftest.py
import pytest
import requests
​
​
class MockResponse:
    @staticmethod
    def json():
        return {"mock_key": "mock_response"}
​
# 在夹具中使用猴子补丁
@pytest.fixture
def mock_response(monkeypatch):
    """Requests.get() 模拟返回 {'mock_key':'mock_response'}."""
    def mock_get(*args, **kwargs):
        return MockResponse()
    monkeypatch.setattr(requests, "get", mock_get)
#content of test_mock1.py
from c import app
def test_get_json(mock_response):
    result = app.get_json("https://www.baidu1.com")
    assert result["mock_key"] == "mock_response"

三、全局补丁示例:防止来自远程的 “requests”

# contents of conftest.py
import pytest
@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
    """为所有的测试移除 requests.sessions.Session.request."""
    monkeypatch.delattr("requests.sessions.Session.request")

这个自动使用的夹具会被每个测试执行,这个夹具会删除 request.session.Session.request 方法,这样,在任何测试中尝试进行http请求的操作都会失败

注意:我们不建议对内置的函数进行补丁操作,例如 open, compile 这种,因为这可能会打破pytest的内部结构。如果这种补丁是不可避免的,你可以尝试在命令行中传递 --tb=native, --assert=plain 和 --capture=no,虽然这种方法不能保证一定有用,但是一些情况下可以解决问题

注意:修补 stdlib pytest使用的函数和一些第三方库可能会破坏pytest本身,因此在这些情况下,建议使用 MonkeyPatch.context() 要将修补限制到要测试的块,请执行以下操作:

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

四、MonkeyPatching环境变量

#content of user.py
import os
def get_os_user_lower():
    """Simple retrieval function.
    Returns lowercase USER or raises OSError."""
    username = os.getenv("USER")
    if username is None:
        raise OSError("USER environment is not set.")
    return username.lower()
​
#content of test_user.py
import pytest
from c.user import get_os_user_lower
def test_upper_to_lower(monkeypatch):
    """Set the USER env var to assert the behavior."""
    monkeypatch.setenv("USER", "TestingUser")
    assert get_os_user_lower() == "testinguser"
​
def test_raise_exception(monkeypatch):
    """Remove the USER env var and assert OSError is raised."""
    monkeypatch.delenv("USER", raising=False)
​
    with pytest.raises(OSError):
        _ = get_os_user_lower()
  

五、MonkeyPatching字典

monkeypatch.setitem 可用于在测试期间安全地将字典值设置为特定值

#content of user1.py
DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
​
def create_connection_string(config=None):
    """Creates a connection string from input or defaults."""
    config = config or DEFAULT_CONFIG
    return "User Id="+config['user']+"+Location="+config['database']
​
#content of test_user1.py
import pytest
​
from c import user1
​
​
def test_connection(monkeypatch):
    monkeypatch.setitem(user1.DEFAULT_CONFIG,'user','test_user')
    monkeypatch.setitem(user1.DEFAULT_CONFIG, "database", "test_db")
    expected = "User Id=test_user+Location=test_db"
    result = user1.create_connection_string()
    assert result == expected
#使用 monkeypatch.delitem 删除值。
def test_missing_user(monkeypatch):
​
    # patch the DEFAULT_CONFIG t be missing the 'user' key
    monkeypatch.delitem(user1.DEFAULT_CONFIG, "user", raising=False)
​
    # Key error expected because a config is not passed, and the
    # default is now missing the 'user' entry.
    with pytest.raises(KeyError):
        print("KeyError!!!!!!")
        _ = user1.create_connection_string()
输出:
..\c\test_user1.py::test_connection PASSED
..\c\test_user1.py::test_missing_user KeyError!!!!!!
PASSED

参考文献:https://www.osgeo.cn/pytest/monkeypatch.html

— 于 共写了5304个字
— 标签:

评论已关闭。