如何模拟with语句中使用的打开(使用Python中的Mock框架)?


188

如何使用模拟测试以下代码(使用模拟Michael Foord的Mock框架提供的补丁装饰器和哨兵):

def testme(filepath):
    with open(filepath, 'r') as f:
        return f.read()

@Daryl Spitzer:您能否忽略元问题(“我知道答案...”),这令人困惑。
S.Lott

过去,当我放弃它时,人们抱怨我在回答自己的问题。我将尝试将其移至我的答案。
达里尔·斯皮策

1
@Daryl:避免抱怨回答自己的问题的最佳方法通常是将问题和/或答案标记为“社区Wiki”,而这种抱怨通常源于对“业力妓女”的担忧。
约翰·米利金

3
如果认为回答您自己的问题被认为是Karma Whoring,则我认为应在这一点上澄清FAQ。
EBGreen

Answers:


132

模拟0.7.0中的更改方式已更改,该模拟最终支持模拟python协议方法(魔术方法),尤其是使用MagicMock:

http://www.voidspace.org.uk/python/mock/magicmock.html

作为上下文管理器打开模拟的示例(来自模拟文档中的示例页面):

>>> open_name = '%s.open' % __name__
>>> with patch(open_name, create=True) as mock_open:
...     mock_open.return_value = MagicMock(spec=file)
...
...     with open('/some/path', 'w') as f:
...         f.write('something')
...
<mock.Mock object at 0x...>
>>> file_handle = mock_open.return_value.__enter__.return_value
>>> file_handle.write.assert_called_with('something')

哇!这比目前在voidspace.org.uk/python/mock/magicmock.html上的上下文管理器示例要简单得多,该示例也显式设置__enter____exit__模拟对象-后一种方法过时了还是仍然有用?
布兰登·罗兹

5
“后期方法”展示了如何在使用MagicMock的情况下进行操作(即,这只是Mock如何支持魔术方法的示例)。如果您使用MagicMock(如上所述),则将为您预先配置进入退出
Fuzzyman 2011年

5
您可以指向您的博客文章,在其中详细说明原因/工作原理
Rodrigue

9
在Python 3中,未定义“文件”(在MagicMock规范中使用),因此我改用io.IOBase。
乔纳森·哈特利

1
注意:在Python3中,内置file函数已消失!
exhuma

239

mock_openmock框架的一部分,使用非常简单。patch用作上下文返回用于替换已修补对象的对象:您可以使用它使测试更简单。

Python 3.x

使用builtins代替__builtin__

from unittest.mock import patch, mock_open
with patch("builtins.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

Python 2.7

mock不属于unittest您,您应该修补__builtin__

from mock import patch, mock_open
with patch("__builtin__.open", mock_open(read_data="data")) as mock_file:
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

装饰箱

如果将的结果用作的patch修饰符用作装饰器mock_open(),则new patch可能会有些奇怪。

在这种情况下,最好使用new_callable patch的参数并要记住,每个patch不使用的额外参数都会按照文档中的new_callable描述传递给函数。patch

patch()使用任意关键字参数。这些将在构造时传递给Mock(或new_callable)。

例如,Python 3.x的修饰版本为:

@patch("builtins.open", new_callable=mock_open, read_data="data")
def test_patch(mock_file):
    assert open("path/to/open").read() == "data"
    mock_file.assert_called_with("path/to/open")

请记住,在这种情况下,patch会将模拟对象添加为测试函数的参数。


抱歉要求,可以将with patch("builtins.open", mock_open(read_data="data")) as mock_file:其转换为装饰器语法吗?我已经尝试过,但是我不确定@patch("builtins.open", ...) 第二个论点需要传递什么。
imrek

1
@DrunkenMaster更新了..谢谢指出。在这种情况下,使用装饰器并非易事。
米歇尔·达米科

谢谢!我的问题是一个复杂一点(我不得不信道return_valuemock_open到另一个模拟对象,并断言第二模拟的return_value),但它的工作,加入mock_open作为new_callable
imrek

1
@ArthurZopellaro看一下six模块是否具有一致的mock模块。但是我不知道它是否也映射builtins在一个通用模块中。
米歇尔·达米科

1
您如何找到要修补的正确名称?即如何为任意函数找到@patch的第一个参数(在这种情况下为“ builtins.open”)?
zenperttu

73

使用最新版本的模拟,您可以使用真正有用的模拟_打开帮助器:

mock_open(模拟=无,读取数据=无)

一个辅助函数创建一个模拟来代替open的使用。它适用于直接调用或用作上下文管理器的open。

模拟参数是要配置的模拟对象。如果没有(默认),则将为您创建一个MagicMock,其API限于标准文件句柄上可用的方法或属性。

read_data是用于返回文件句柄的read方法的字符串。默认情况下,这是一个空字符串。

>>> from mock import mock_open, patch
>>> m = mock_open()
>>> with patch('{}.open'.format(__name__), m, create=True):
...    with open('foo', 'w') as h:
...        h.write('some stuff')

>>> m.assert_called_once_with('foo', 'w')
>>> handle = m()
>>> handle.write.assert_called_once_with('some stuff')

您如何检查是否有多个.write通话?
n611x007

1
@naxa一种方法是将每个预期参数传递给handle.write.assert_any_call()handle.write.call_args_list如果顺序很重要,您还可以使用来获取每个呼叫。
罗伯·卡特莫

m.return_value.write.assert_called_once_with('some stuff')更好的imo。避免注册电话。
匿名2016年

2
手动声明about Mock.call_args_list比调用任何Mock.assert_xxx方法更安全。如果您拼写的后者(作为Mock的属性)的拼写错误,它们将始终默默地通过。
乔纳森·哈特利

12

要对一个简单文件使用嘲笑read()(打开)(此页面上已经给出的原始嘲笑的片段更适合写入):

my_text = "some text to return when read() is called on the file object"
mocked_open_function = mock.mock_open(read_data=my_text)

with mock.patch("__builtin__.open", mocked_open_function):
    with open("any_string") as f:
        print f.read()

请注意,根据嘲笑文档的doc,这是专门针对的read(),因此不适for line in f用于像这样的常见模式。

使用python 2.6.6 /模拟1.0.1


看起来不错,但是我无法使其与for line in opened_file:代码类型一起使用。我尝试__iter__使用实现的可迭代StringIO进行实验,并使用它代替my_text,但是没有运气。
叶夫根(Evgen)

@EvgeniiPuchkaryov此功能专门用于read()您的for line in opened_file情况;我已编辑帖子以澄清
jlb83 2015年

1
@EvgeniiPuchkaryov for line in f:支持可以通过将String 的返回值模拟open()StringIO对象来实现
Iskar Jarak 2015年

1
为了明确起见,本例中的被测系统(SUT)为: with open("any_string") as f: print f.read()
Brad M

4

最佳答案很有用,但我对此进行了扩展。

如果要基于传递给此处的参数设置文件对象(fin as f)的值,open()这是一种实现方法:

def save_arg_return_data(*args, **kwargs):
    mm = MagicMock(spec=file)
    mm.__enter__.return_value = do_something_with_data(*args, **kwargs)
    return mm
m = MagicMock()
m.side_effect = save_arg_return_array_of_data

# if your open() call is in the file mymodule.animals 
# use mymodule.animals as name_of_called_file
open_name = '%s.open' % name_of_called_file

with patch(open_name, m, create=True):
    #do testing here

基本上,open()将返回一个对象并with调用__enter__()该对象。

为了正确模拟,我们必须模拟open()返回一个模拟对象。然后,该模拟对象应该模拟对其的__enter__()调用(MagicMock将为我们执行此操作),以返回我们想要的模拟数据/文件对象(因此mm.__enter__.return_value)。通过上面的方法使用2个模拟进行此操作,我们可以捕获传递给参数的参数open()并将其传递给我们的do_something_with_data方法。

我将整个模拟文件作为字符串传递给了open()我,do_something_with_data如下所示:

def do_something_with_data(*args, **kwargs):
    return args[0].split("\n")

这会将字符串转换为列表,因此您可以像处理普通文件一样执行以下操作:

for line in file:
    #do action

如果要测试的代码以其他方式处理文件,例如通过调用其函数“ readline”,则可以在函数“ do_something_with_data”中返回具有所需属性的任何模拟对象。
user3289695'9

有办法避免触摸__enter__吗?绝对看起来像是骇客,而不是推荐的方式。
imrek

enter是如何编写像open()这样的conext管理器的。嘲弄通常会有点笨拙,因为您需要访问“私人”东西进行嘲笑,但是此处输入的内容并不是故意怪异的imo
播音员

3

我可能对游戏有些迟了,但是当我调用open另一个模块而不必创建新文件时,这对我有用。

test.py

import unittest
from mock import Mock, patch, mock_open
from MyObj import MyObj

class TestObj(unittest.TestCase):
    open_ = mock_open()
    with patch.object(__builtin__, "open", open_):
        ref = MyObj()
        ref.save("myfile.txt")
    assert open_.call_args_list == [call("myfile.txt", "wb")]

MyObj.py

class MyObj(object):
    def save(self, filename):
        with open(filename, "wb") as f:
            f.write("sample text")

通过open__builtin__模块内部的功能修补到my mock_open(),可以模拟写入文件而无需创建文件。

注意:如果您正在使用使用cython的模块,或者您的程序以任何方式依赖cython,则需要通过在文件顶部包含来导入cython的__builtin__模块import __builtin____builtin__如果您使用cython,则将无法模拟通用。


这种方法的变体对我有用,因为大多数受测试的代码在其他模块中,如下所示。我确实需要确保将其添加import __builtin__到我的测试模块中。本文有助于阐明该技术为何能发挥出色的作用:ichimonji10.name/blog/6
killthrush 2015年

0

要使用unittest修补内置的open()函数:

这适用于读取json配置的补丁。

class ObjectUnderTest:
    def __init__(self, filename: str):
        with open(filename, 'r') as f:
            dict_content = json.load(f)

模拟对象是open()函数返回的io.TextIOWrapper对象。

@patch("<src.where.object.is.used>.open",
        return_value=io.TextIOWrapper(io.BufferedReader(io.BytesIO(b'{"test_key": "test_value"}'))))
    def test_object_function_under_test(self, mocker):

0

如果您不再需要任何文件,则可以装饰测试方法:

@patch('builtins.open', mock_open(read_data="data"))
def test_testme():
    result = testeme()
    assert result == "data"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.