5, pytest Chinese documents - Monkey Patch

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 monkeypatchCan help you secure set / delete a property, dictionary entries or environment variables, and even change when importing module sys.pathpath.

monkeypatchIt 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 fixturerevoked after execution is complete. raisingParameter indicates: When setting / deletion operation of destination does not exist, whether to report KeyErrorand AttributeErroranomalies.

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()modification Path.homemethod, during the test run, it has been returned is fixed Path("/abc"), so that it is removed in dependence on different platforms; After the test run is completed, the Path.homemodifications 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 urlreturn 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 returns bytesdata type; we can define a class in place of the test module 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'

    You can continue to build a more actual scene complexity MockResponse; for example, you can always comprise a return Trueof the okproperty, or a character string input read()return different values;

    We can also fixturecross 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 fixturefrom the original mock_responsereplaced monkeypatch;
    • Because monkeypatcha functionlevel scope, it mock_responsecan only be functionlevel, otherwise it will report ScopeMismatch: You tried to access the 'function' scoped fixture 'monkeypatch' with a 'module' scoped request objectan error;
    • If you want to mock_responseapply to all test cases, you can consider it to conftest.pyinside and marks autouse=True;
  • In this example, use the monkeypatch.delattr()delete urllib.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 pytestthe use of the library, may contaminate pytestitself, it is recommended to use MonkeyPatch.context(), it returns an MonkeyPatchobject that combine withto 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 monkeypatchof 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 raisingto 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.delitemto 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

Guess you like

Origin www.cnblogs.com/luizyao/p/11698702.html