【原文链接】
一、猴子补丁简介
在有些场景下的测试可能需要修改全局配置或者系统变量等操作,而这些操作仅仅是为了做一些测试,不希望永久的修改,此时就需要使用猴子补丁了,猴子补丁,即monkeypatch,是一个fixture,它提供了以下方法:
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.setattr("somemodule.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=None)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
当测试结束后或者fixture执行完成后,monkeypatch中做的所有修改都将恢复
二、>通过猴子补丁临时修改函数功能
如下,可以通过猴子补丁修改Path的home属性,进而临时修改函数的功能,然后进行测试,这样测试结束后,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")
def test_home():
print(Path.home())
执行结果如下,很明显,在test_home测试函数中,Path.home属性并没有发生修改
$ pytest -s -v
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- D:\Python39\python.exe
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('D:\\src\\blog\\tests\\.hypothesis\\examples')
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 2 items
test_demo.py::test_getssh PASSED
test_demo.py::test_home C:\Users\hitre
PASSED
========================================================================== 2 passed in 0.13s ===========================================================================
三、通过猴子补丁取消测试函数中request的使用
在conftest.py中编写如下代码即可
import pytest
@pytest.fixture(autouse=True)
def no_requests(monkeypatch):
"""Remove requests.sessions.Session.request for all tests."""
monkeypatch.delattr("requests.sessions.Session.request")
四、通过猴子补丁对环境变量测测试
如下,假设get_os_user_lower函数为被测函数,用例中可以通过猴子补丁对变量进行临时设置或删除,这样可以保证测试用例的准确性,否则当环境变量被修改或者被删除后,用例的稳定性将会收到影响
test_demo.py代码如下:
import os
import pytest
def get_os_user_lower():
username = os.getenv("USER")
if username is None:
raise OSError("USER environment is not set.")
return username.lower()
def test_upper_to_lower(monkeypatch):
monkeypatch.setenv("USER", "TestingUser")
assert get_os_user_lower() == "testinguser"
def test_raise_exception(monkeypatch):
monkeypatch.delenv("USER", raising=False)
with pytest.raises(OSError):
_ = get_os_user_lower()
执行结果如下:
$ pytest -v
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- D:\Python39\python.exe
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('D:\\src\\blog\\tests\\.hypothesis\\examples')
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 2 items
test_demo.py::test_upper_to_lower PASSED [ 50%]
test_demo.py::test_raise_exception PASSED [100%]
========================================================================== 2 passed in 0.13s ===========================================================================
上述代码可以通过fixture继续优化如下:
import os
import pytest
def get_os_user_lower():
username = os.getenv("USER")
if username is None:
raise OSError("USER environment is not set.")
return username.lower()
@pytest.fixture
def mock_env_user(monkeypatch):
monkeypatch.setenv("USER", "TestingUser")
@pytest.fixture
def mock_env_missing(monkeypatch):
monkeypatch.delenv("USER", raising=False)
def test_upper_to_lower(mock_env_user):
assert get_os_user_lower() == "testinguser"
def test_raise_exception(mock_env_missing):
with pytest.raises(OSError):
_ = get_os_user_lower()
执行结果如下:
$ pytest -v
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- D:\Python39\python.exe
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('D:\\src\\blog\\tests\\.hypothesis\\examples')
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 2 items
test_demo.py::test_upper_to_lower PASSED [ 50%]
test_demo.py::test_raise_exception PASSED [100%]
========================================================================== 2 passed in 0.13s ===========================================================================
五、通过猴子补丁对字典数据模拟测试
test_demo.py代码如下,其中DEFAULT_CONFIG为被测字典,create_connection_string为被测函数,测试当被测字典被修改或者被删除时的情况
import pytest
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(DEFAULT_CONFIG, "user", "test_user")
monkeypatch.setitem(DEFAULT_CONFIG, "database", "test_db")
expected = "User Id=test_user; Location=test_db;"
result = create_connection_string()
assert result == expected
def test_missing_user(monkeypatch):
monkeypatch.delitem(DEFAULT_CONFIG, "user", raising=False)
with pytest.raises(KeyError):
_ = create_connection_string()
执行结果如下:
$ pytest -v
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- D:\Python39\python.exe
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('D:\\src\\blog\\tests\\.hypothesis\\examples')
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 2 items
test_demo.py::test_connection PASSED [ 50%]
test_demo.py::test_missing_user PASSED [100%]
========================================================================== 2 passed in 0.13s ===========================================================================
上述代码可以通过fixture进行优化,如下:
import pytest
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']};"
@pytest.fixture
def mock_test_user(monkeypatch):
monkeypatch.setitem(DEFAULT_CONFIG, "user", "test_user")
@pytest.fixture
def mock_test_database(monkeypatch):
monkeypatch.setitem(DEFAULT_CONFIG, "database", "test_db")
@pytest.fixture
def mock_missing_default_user(monkeypatch):
monkeypatch.delitem(DEFAULT_CONFIG, "user", raising=False)
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = create_connection_string()
assert result == expected
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = create_connection_string()
执行结果如下:
$ pytest -v
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- D:\Python39\python.exe
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('D:\\src\\blog\\tests\\.hypothesis\\examples')
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 2 items
test_demo.py::test_connection PASSED [ 50%]
test_demo.py::test_missing_user PASSED [100%]
========================================================================== 2 passed in 0.12s ===========================================================================