table of Contents
Occasionally, test cases need to call some functions are dependent on global configuration, the functions or some code in turn calls the test is not easy (e.g.: network access). fixture monkeypatch
Can help you secure set / delete a property, dictionary entries or environment variables, and even change when importing module sys.path
path.
monkeypatch
It provides the following methods:
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)
All changes will be the test case or fixture
revoked after execution is complete. raising
Parameter indicates: When setting / deletion operation of destination does not exist, whether to report KeyError
and AttributeError
anomalies.
1. Modify the class properties or function-
Use monkeypatch.setattr()
can be modified to your property or function desired behavior, use monkeypatch.delattr()
can be deleted using the function or property of the test case;
Reference to the following three examples:
In this example, a
monkeypatch.setattr()
modificationPath.home
method, during the test run, it has been returned is fixedPath("/abc")
, so that it is removed in dependence on different platforms; After the test run is completed, thePath.home
modifications will be revoked;# src/chapter-5/test_module.py from pathlib import Path def getssh(): return Path.home() / ".ssh" def test_getssh(monkeypatch): def mockreturn(): return Path("/abc") # 替换 Path.home # 需要在真正的调用之前执行 monkeypatch.setattr(Path, "home", mockreturn) # 将会使用 mockreturn 代替 Path.home x = getssh() assert x == Path("/abc/.ssh")
In this example, a
monkeypatch.setattr()
combined type, the function returns the object simulation;Suppose we have a simple function to access a
url
return to the page content:# src/chapter-5/app.py from urllib import request def get(url): r = request.urlopen(url) return r.read().decode('utf-8')
We are now going to simulate
r
, it requires a.read()
method returnsbytes
data type; we can define a class in place of the test moduler
:# src/chapter-5/test_app.py from urllib import request from app import get # 自定义的类模拟 urlopen 的返回值 class MockResponse: # 永远返回一个固定的 bytes 类型的数据 @staticmethod def read(): return b'luizyao.com' def test_get(monkeypatch): def mock_urlopen(*args, **kwargs): return MockResponse() # 使用 request.mock_urlopen 代替 request.urlopen monkeypatch.setattr(request, 'urlopen', mock_urlopen) data = get('https://luizyao.com') assert data == 'luizyao.com'
You can continue to build a more actual scene complexity
MockResponse
; for example, you can always comprise a returnTrue
of theok
property, or a character string inputread()
return different values;We can also
fixture
cross the shared use cases:# src/chapter-5/test_app.py import pytest # monkeypatch 是 function 级别作用域的,所以 mock_response 也只能是 function 级别, # 否则会报 ScopeMismatch @pytest.fixture def mock_response(monkeypatch): def mock_urlopen(*args, **kwargs): return MockResponse() # 使用 request.mock_urlopen 代替 request.urlopen monkeypatch.setattr(request, 'urlopen', mock_urlopen) # 使用 mock_response 代替原先的 monkeypatch def test_get_fixture1(mock_response): data = get('https://luizyao.com') assert data == 'luizyao.com' # 使用 mock_response 代替原先的 monkeypatch def test_get_fixture2(mock_response): data = get('https://bing.com') assert data == 'luizyao.com'
note:
- Use of the test
fixture
from the originalmock_response
replacedmonkeypatch
; - Because
monkeypatch
afunction
level scope, itmock_response
can only befunction
level, otherwise it will reportScopeMismatch: You tried to access the 'function' scoped fixture 'monkeypatch' with a 'module' scoped request object
an error; - If you want to
mock_response
apply to all test cases, you can consider it toconftest.py
inside and marksautouse=True
;
- Use of the test
In this example, use the
monkeypatch.delattr()
deleteurllib.request.urlopen()
method;# src/chapter-5/test_app.py @pytest.fixture def no_request(monkeypatch): monkeypatch.delattr('urllib.request.urlopen') def test_delattr(no_request): data = get('https://bing.com') assert data == 'luizyao.com'
carried out:
λ pipenv run pytest --tb=native --assert=plain --capture=no src/chapter-5/test_app. py::test_delattr =============================== test session starts ================================ platform win32 -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 rootdir: D:\Personal Files\Projects\pytest-chinese-doc collected 1 item src\chapter-5\test_app.py F ===================================== FAILURES ===================================== ___________________________________ test_delattr ___________________________________ Traceback (most recent call last): File "D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-5\test_app.py", line 78, in test_delattr data = get('https://bing.com') File "D:\Personal Files\Projects\pytest-chinese-doc\src\chapter-5\app.py", line 26, in get r = request.urlopen(url) AttributeError: module 'urllib.request' has no attribute 'urlopen' ================================ 1 failed in 0.04s =================================
note:
Avoid deleting method library built, if we want to do, the best plus
--tb=native --assert=plain --capture=no
;Modify
pytest
the use of the library, may contaminatepytest
itself, it is recommended to useMonkeyPatch.context()
, it returns anMonkeyPatch
object that combinewith
to limit these changes occur only in the package code.def test_stdlib(monkeypatch): with monkeypatch.context() as m: m.setattr(functools, "partial", 3) assert functools.partial == 3
2. modify environment variables
Use monkeypatch
of setenv()
and delenv()
methods can be safely set / remove environment variables in the test;
# src/chapter-5/test_env.py
import os
import pytest
def get_os_user():
username = os.getenv('USER')
if username is None:
raise IOError('"USER" environment variable is not set.')
return username
def test_user(monkeypatch):
monkeypatch.setenv('USER', 'luizyao')
assert get_os_user() == 'luizyao'
def test_raise_exception(monkeypatch):
monkeypatch.delenv('USER', raising=False)
pytest.raises(IOError, get_os_user)
monkeypatch.delenv()
It is raising
to be set False
, or it may be reported KeyError
;
You can also use fixture
, cross-sharing use cases:
import pytest
@pytest.fixture
def mock_env_user(monkeypatch):
monkeypatch.setenv("USER", "TestingUser")
@pytest.fixture
def mock_env_missing(monkeypatch):
monkeypatch.delenv("USER", raising=False)
# notice the tests reference the fixtures for mocks
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()
3. Modify the dictionary
Use monkeypatch.setitem()
method can safely modify the dictionary specified value during testing;
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']};"
We can modify the user database or other databases:
import app
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
It can be used monkeypatch.delitem
to delete specific items:
import pytest
import app
def test_missing_user(monkeypatch):
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
with pytest.raises(KeyError):
_ = app.create_connection_string()
GitHub repository Address: https://github.com/luizyao/pytest-chinese-doc