Python模拟内置的print函数

53

我已经尝试过

from mock import Mock
import __builtin__

__builtin__.print = Mock()

但是这会引发一个语法错误。我也尝试过像这样修补它:

@patch('__builtin__.print')
def test_something_that_performs_lots_of_prints(self, mock_print):

    # assert stuff

有没有办法做到这一点?


你使用的是哪个版本的Python? - Michael Mauderer
Python 2.7.3,虽然很想了解如何在3下执行,如果在2.7.x下无法实现的话。 - aychedee
2
相关:https://dev59.com/tmgu5IYBdhLWcg3wuJSF - ecatmur
2
是的,在测试情况下,模拟sys.stdout可能是最好的方法。 - aychedee
12个回答

55

我知道已经有一个被接受的答案了,但是对于那个问题有一个更简单的解决方案-在Python 2.x中模拟打印。答案在模拟库教程中:http://www.voidspace.org.uk/python/mock/patch.html,它是:

>>> from StringIO import StringIO
>>> def foo():
...     print 'Something'
...
>>> @patch('sys.stdout', new_callable=StringIO)
... def test(mock_stdout):
...     foo()
...     assert mock_stdout.getvalue() == 'Something\n'
...
>>> test()

当然,你也可以使用以下断言:

self.assertEqual("Something\n", mock_stdout.getvalue())

我已在我的单元测试中检查了这个解决方案,它按预期工作。希望这能帮助到某人。祝好!


这在 Python 2 的 pytest 中不起作用(对于注释中的格式化表示抱歉)。"def test_status(mocker): mocker_print = mocker.patch("sys.stdout", new_callable=StringIO)
print("yes")E TypeError: unicode argument expected, got 'str'"
- Yaroslav Nikitenko
从Python 3开始,您需要从io中导入StringIO,如下所示:from io import StringIO - FiddleStix

43

这是一个更简单的Python 3解决方案--直接在内置的print函数上使用unittest.mock,而不需调整sys.stdout

from unittest.mock import patch, call

@patch('builtins.print')
def test_print(mocked_print):
    print('foo')
    print()

    assert mocked_print.mock_calls == [call('foo'), call()]

另一种示例,直接评估argskwargs:

@patch('builtins.print')
def test_print(mocked_print):
    print('foo', 'bar', file=sys.stderr)

    assert mocked_print.call_args.args == ('foo', 'bar')
    assert mocked_print.call_args.kwargs == dict(file=sys.stderr)

1
这是Python 3中唯一有意义的方法。这样我们就可以对模拟对象执行断言,这对于测试来说是一个重大优势。 - Artur
感谢您提供的答案,这正是我最终采用的方法。不过我注意到更好的做法是将 patch 限制在我期望使用 print 的上下文中(导入模块的位置),否则我会对 pytest、pdb 等使用的内置函数进行 patch ,举个例子,这会阻止我进入测试代码。因此,假设调用发生在 myapp.mymod 模块中,我会选择 patch myapp.mymod.print。 - Matteo Mecucci
如何检查调用print()并提供file=sys.stderr参数的情况? - chrisinmtown
如果您想重新创建打印的内容(假设您一次只传递一个参数给print,因为您可能使用f-strings),则可以使用以下代码:printed = '\n'.join(x.args[0] for x in mocked_print.mocks_calls) - run_the_race

18

print 是 Python 2.x 中的一个关键字,如果将其用作属性会引发 SyntaxError 错误。你可以在文件开头使用 from __future__ import print_function 来避免这种错误。

注意:你不能简单地使用 setattr,因为除非禁用 print 语句,否则你修改后的 print 函数不会被调用。

编辑:你还需要在想要使用修改后的 print 函数的每个文件中都使用 from __future__ import print_function,否则它会被 print 语句掩盖。


那我可以使用 setattr(__builtin__, 'print', Mock()),然后以某种方式禁用打印语句吗?或者你是指通过导入Python 3的打印函数来禁用它?如果能够完全在测试端上实现而不修改被测试代码,那就太好了。 - aychedee
@aychedee 在所有需要使用您修改后的 print 函数的源文件中,您需要通过导入 Python 3 的 print 函数来禁用它。在 lqc 的方式中使用的 setattr 将无法工作,因为它被 print 语句掩盖了。 - quantum
1
好的,谢谢xiaomao。没问题。所以答案是只有使用Python 3的print函数才能实现,没有它就不行。 - aychedee
请提供一个完整的示例,如何测试对 print 的实际调用? - Yaroslav Nikitenko

6
from unittest.mock import patch


def greet():
    print("Hello World")


@patch('builtins.print')
def test_greet(mock_print):
    greet()
    mock_print.assert_called_with("Hello World!")

看起来这个解决方案已经在另一个答案中提出了。 - Sergey Shubin

3

我的版本。

在被测试的程序中(例如:pp.py):

from __future__ import print_function

def my_func():
    print('hello')

在测试程序中:
def test_print(self):
    from pp import my_func
    from mock import call
    with mock.patch('__builtin__.print') as mock_print:
       my_func()
       mock_print.assert_has_calls(
            [
                call('hello')
            ]
        )

3
如果你想坚持使用2.x版本的print语句而不是2.x版本中的print()函数,那么你可以模拟你的sys.stdout
写一个虚拟的“文件”,大概可以这样写:
class Writable(object):
    """Class which has the capability to replace stdout."""
    newwrite = None
    def __init__(self, oldstdout, newwrite=None):
        self.oldstdout = oldstdout
        if newwrite is not None:
            self.newwrite = newwrite
    def write(self, data):
        self.newwrite(self.oldstdout, data)
    @classmethod
    def subclass(cls, writefunc):
        newcls = type('', (cls,),
            dict(write=lambda self, data: writefunc(self.oldstdout, data)
        return newcls

这个类期望与一个写入函数结合使用,该函数获取打印的数据。这个写入函数应该有两个参数:第一个参数是要在最后用于打印的“旧标准输出”,第二个参数是数据。

让我们看一个例子:

def mywrite(sink, data):
    sink.write(data.encode("hex"))

为此需要做的。

现在你可以做到

import sys
sys.stdout = Writable(sys.stdout, mywrite)

或者你可以这样做。
@Writable.subclass
def mywritable(sink, data)
    sink.write(data.encode("hex"))

sys.stdout = mywritable(sys.stdout)

第二个版本有点棘手:它通过装饰器函数创建一个Writable的子类,将给定的函数转换为新类的方法,并放入给定函数的名称中。
之后,你就有了一个新类,可以用“旧的stdout”作为参数进行实例化,然后替换sys.stdout

1
import mock
import sys

mock_stdout = mock.Mock()
sys.stdout = mock_stdout
print 'Hello!'
sys.stdout = sys.__stdout__

print mock_stdout.mock_calls
[call.write('Hello!'), call.write('\n')]

1
如果您能解释一下您的做法以及为什么问题中的代码无法工作,那将非常有帮助。 - blalasaadri
我们正在通过我们的模拟对象切换sys.stdout,当我们打印一些文本时,它可能会在mock_stdout调用中找到。最后,我们将sys.stdout返回到原始状态。 - sgjurano

1
这是@KC答案的v3版本。 我不想嘲笑print,因为我特别想看到整个输出,而不是检查单个调用,所以StringIO对我来说更有意义。
from io import StringIO
from unittest.mock import patch

def foo():
    print ('Something')

def test():
    with patch('sys.stdout', new_callable=StringIO) as buffer:
        foo()
    fake_stdout = buffer.getvalue()

    #print() is back!
    print(f"fake_stdout:{fake_stdout}")
    assert fake_stdout == 'Something\n'

test()

警告:

在修补程序的整个过程中,模拟标准输出与使用 pdb.set_trace() 不兼容。我注释掉了 with...,添加了 if True: 以保持缩进,调试了我的脚本,并在修复错误后重新添加了批处理。

    #with patch('sys.stdout', new_callable=StringIO) as buffer:
    if True:
        foo()
    ...

0

这个 Python 3 的例子是在 Krzysztof 的 Python 2 答案基础上构建的。它使用了 unittest.mock。它还使用了一个可重用的辅助方法来进行断言。

import io
import unittest
import unittest.mock

from .solution import fizzbuzz


class TestFizzBuzz(unittest.TestCase):

    @unittest.mock.patch('sys.stdout', new_callable=io.StringIO)
    def assert_stdout(self, n, expected_output, mock_stdout):
        fizzbuzz(n)
        self.assertEqual(mock_stdout.getvalue(), expected_output)

    def test_only_numbers(self):
        self.assert_stdout(2, '1\n2\n')

0

还可以使用assert_any_call

from unittest.mock import patch, call

@patch('builtins.print')
def test_print(mocked_print):
    print('foo')
    print()

   mocked_print.assert_any_call('foo')

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