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參數表明:當設置/刪除操作的目標不存在時,是否上報KeyErrorAttributeError異常。

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;例如,你可以包含一個總是返回Trueok屬性,或者根據輸入的字符串爲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
    • 因爲monkeypatchfunction級別作用域的,所以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. 修改環境變量

使用monkeypatchsetenv()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倉庫地址:https://github.com/luizyao/pytest-chinese-doc

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章