目錄python
有時候,測試用例須要調用某些依賴於全局配置的功能,或者這些功能自己又調用了某些不容易測試的代碼(例如:網絡接入)。fixture monkeypatch
能夠幫助你安全的設置/刪除一個屬性、字典項或者環境變量,甚至改變導入模塊時的sys.path
路徑。git
monkeypatch
提供瞭如下方法:github
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
異常。數據庫
使用monkeypatch.setattr()
能夠將函數或者屬性修改成你但願的行爲,使用monkeypatch.delattr()
能夠刪除測試用例使用的函數或者屬性;安全
參考如下三個例子:bash
在這個例子中,使用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()
結合類,模擬函數的返回對象;session
假設咱們有一個簡單的功能,訪問一個url
返回網頁內容:app
# 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
:ssh
# 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
使用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()
使用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()