欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

5、pytest 中文文档--猴子补丁

程序员文章站 2022-05-22 16:30:38
有时候,测试用例需要调用某些依赖于全局配置的功能,或者这些功能本身又调用了某些不容易测试的代码(例如:网络接入)。`fixture monkeypatch`可以帮助你安全的**设置/删除**一个属性、字典项或者环境变量,甚至改变导入模块时的`sys.path`路径。 ......

目录

有时候,测试用例需要调用某些依赖于全局配置的功能,或者这些功能本身又调用了某些不容易测试的代码(例如:网络接入)。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仓库地址: