5、pytest 中文文档--猴子补丁
目录
有时候,测试用例需要调用某些依赖于全局配置的功能,或者这些功能本身又调用了某些不容易测试的代码(例如:网络接入)。fixture monkeypatch
可以帮助你安全的设置/删除一个属性、字典项或者环境变量,甚至改变导入模块时的sys.path
路径。
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)
所有的修改将在测试用例或者fixture
执行完成后撤销。raising
参数表明:当设置/删除操作的目标不存在时,是否上报keyerror
和attributeerror
异常。
1. 修改函数功能或者类属性
使用monkeypatch.setattr()
可以将函数或者属性修改为你希望的行为,使用monkeypatch.delattr()
可以删除测试用例使用的函数或者属性;
参考以下三个例子:
-
在这个例子中,使用
monkeypatch.setattr()
修改path.home
方法,在测试运行期间,它一直返回的是固定的path("/abc")
,这样就移除了它在不同平台上的依赖;测试运行完成后,对path.home
的修改会被撤销;# 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")
-
在这个例子中,使用
monkeypatch.setattr()
结合类,模拟函数的返回对象;假设我们有一个简单的功能,访问一个
url
返回网页内容:# src/chapter-5/app.py from urllib import request def get(url): r = request.urlopen(url) return r.read().decode('utf-8')
我们现在要去模拟
r
,它需要一个.read()
方法返回的是bytes
的数据类型;我们可以在测试模块中定义一个类来代替r
:# 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'
你可以继续为实际的场景构建更具有复杂度的
mockresponse
;例如,你可以包含一个总是返回true
的ok
属性,或者根据输入的字符串为read()
返回不同的值;我们也可以通过
fixture
跨用例共享:# 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'
注意:
- 测试用例使用的
fixture
由原先的mock_response
替换为monkeypatch
; - 因为
monkeypatch
是function
级别作用域的,所以mock_response
也只能是function
级别,否则会报scopemismatch: you tried to access the 'function' scoped fixture 'monkeypatch' with a 'module' scoped request object
错误; - 如果你想让
mock_response
应用于所有的测试用例,可以考虑将它移到conftest.py
里面,并标记autouse=true
;
- 测试用例使用的
-
在这个例子中,使用
monkeypatch.delattr()
删除urllib.request.urlopen()
方法;# 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'
执行:
λ 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 =================================
注意:
避免删除内置库中的方法,如果一定要这么做,最好加上
--tb=native --assert=plain --capture=no
;-
修改
pytest
使用到的库,可能会污染pytest
本身,建议使用monkeypatch.context()
,它返回一个monkeypatch
对象,结合with
限制这些修改只发生在包裹的代码中。def test_stdlib(monkeypatch): with monkeypatch.context() as m: m.setattr(functools, "partial", 3) assert functools.partial == 3
2. 修改环境变量
使用monkeypatch
的setenv()
和delenv()
方法,可以在测试中安全的设置/删除环境变量;
# 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()
的raising
要设置为false
,否则可能会报keyerror
;
你也可以使用fixture
,实现跨用例共享:
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. 修改字典
使用monkeypatch.setitem()
方法可以在测试期间安全的修改字典中特定的值;
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']};"
我们可以修改数据库的用户或者使用其它的数据库:
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
可以使用monkeypatch.delitem
删除指定的项:
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仓库地址: