Python模拟(mock) Patch os.environ和返回值

110

使用模拟(mock)对conn()进行单元测试:

app.py

import mysql.connector
import os, urlparse


def conn():
    if "DATABASE_URL" in os.environ:
        url = urlparse(os.environ["DATABASE_URL"])
        g.db = mysql.connector.connect(
            user=url.username,
            password=url.password,
            host=url.hostname,
            database=url.path[1:],
        )
    else:
        return "Error"

test.py

def test_conn(self):
    with patch(app.mysql.connector) as mock_mysql:
        with patch(app.os.environ) as mock_environ:
            con()
            mock_mysql.connect.assert_callled_with("credentials")

错误:断言 mock_mysql.connect.assert_called_with 没有被调用。

我认为这是因为我的补丁os.environ中没有'Database_url',因此没有对mysql_mock.connect进行测试调用。

问题:

  1. 我需要做哪些更改才能使此测试代码正常工作?

  2. 我是否还需要补丁urlparse

7个回答

150
你可以尝试使用unittest.mock.patch.dict解决方案。只需使用dummy参数调用conn即可:
import mysql.connector
import os, urlparse
from unittest import mock


@mock.patch.dict(os.environ, {"DATABASE_URL": "mytemp"}, clear=True)  # why need clear=True explained here https://dev59.com/AlwZ5IYBdhLWcg3wVfAC#67477901
# If clear is true then the dictionary will be cleared before the new values are set.
def conn(mock_A):
    print os.environ["mytemp"]
    if "DATABASE_URL" in os.environ:
        url = urlparse(os.environ["DATABASE_URL"])
        g.db = mysql.connector.connect(
            user=url.username,
            password=url.password,
            host=url.hostname,
            database=url.path[1:],
        )
    else:
        return "Error"

或者如果你不想修改原来的函数,可以尝试这个解决方案:

import os
from unittest import mock

def func():
    print os.environ["mytemp"]


def test_func():
    k = mock.patch.dict(os.environ, {"mytemp": "mytemp"})
    k.start()
    func()
    k.stop()


test_func()

谢谢,我后来意识到了,很快就删除了我的评论。非常感谢,它正在工作,但我很困惑,使用上述方法如何将 {'mytemp':'mytemp'} 传递到 os.environ 中。 - immrsteel
1
非常感谢,我已经接受了答案,并且在我达到15声望时会点赞该答案。 - immrsteel
由于它对问题/答案没有影响,我从两个地方删除了错误的Python代码 :-) - Martin Thoma
1
太棒了! - Javi Torre
我只想强调 patch.dict() 选项 clear=True!我直觉地认为这应该是默认行为,而且 unittest 文档示例在技术上是正确的但给出了错误的印象,没有突出显示 clear 选项。 - Mike B

51

因此,我发现pytest的monkeypatch装置在需要设置环境变量时会导致更好的代码:

def test_conn(monkeypatch):
    monkeypatch.setenv('DATABASE_URL', '<URL WITH CREDENTIAL PARAMETERS>')
    with patch(app.mysql.connector) as mock_mysql:
        conn()
    mock_mysql.connect.assert_called_with(<CREDENTIAL PARAMETERS>)

这个代码让我遇到了错误: AttributeError: 'MockFixture' object has no attribute 'setenv' - Sonic Soul
1
这对我有效,使用pytest==3.5.0。@SonicSoul也许你的版本不同? - r44
5
请确保在测试参数中包含“monkeypatch”。 - Calvin Li

19
接受的答案是正确的。这里有一个装饰器@mockenv可以做同样的事情。请注意,这不依赖于monkeypatch,所以它应该可以在pytest之外工作。
def mockenv(**envvars):
    return mock.patch.dict(os.environ, envvars)


@mockenv(DATABASE_URL="foo", EMAIL="bar@gmail.com")
def test_something():
    assert os.getenv("DATABASE_URL") == "foo"


15

在我的使用场景中,我试图模拟没有设置任何环境变量的情况。为了做到这一点,请确保您在补丁中添加 clear=True

with patch.dict(os.environ, {}, clear=True):
    func()

2
这正是我所缺少的,谢谢! - Andrew Vaccaro

13
在导入模块之前,在文件模拟环境的头部:
with patch.dict(os.environ, {'key': 'mock-value'}):
    import your.module

2
这对我很有帮助,因为我需要的键和值在导入时就已经被要求,而不是在函数调用时。 - Yirmi

3
您也可以使用类似于此问题中描述的modified_environ上下文管理器来设置/恢复环境变量。
with modified_environ(DATABASE_URL='mytemp'):
    func()

2

这里的回答进行了小改进。

@mock.patch.dict(os.environ, {"DATABASE_URL": "foo", "EMAIL": "bar@gmail.com"})
def test_something():
    assert os.getenv("DATABASE_URL") == "foo"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接