Python:编写控制台打印的单元测试

83

foo函数在控制台中打印内容。我想测试控制台上的输出。我该如何在Python中达到这个目的?

需要测试此函数,没有返回语句:

def foo(inStr):
   print "hi"+inStr

我的测试:

def test_foo():
    cmdProcess = subprocess.Popen(foo("test"), stdout=subprocess.PIPE)
    cmdOut = cmdProcess.communicate()[0]
    self.assertEquals("hitest", cmdOut)

1
将Python内置的print函数转换为内置函数,可以使用future或assert替换stdout文件。 - kwarunek
我不想嘲笑任何东西。事实上,我的实际 foo 接受大约8个参数,并返回一个json。我也想测试这个。 - sudhishkr
7个回答

99

你可以通过将sys.stdout暂时重定向到一个StringIO对象来轻松地捕获标准输出,方法如下:

import StringIO
import sys

def foo(inStr):
    print "hi"+inStr

def test_foo():
    capturedOutput = StringIO.StringIO()          # Create StringIO object
    sys.stdout = capturedOutput                   #  and redirect stdout.
    foo('test')                                   # Call unchanged function.
    sys.stdout = sys.__stdout__                   # Reset redirect.
    print 'Captured', capturedOutput.getvalue()   # Now works as before.

test_foo()
此程序的输出为:
Captured hitest

显示重定向已成功捕获输出并且您能够将输出流恢复到开始捕获之前的状态。


请注意,上面的代码是针对 Python 2.7 的,如问题所示。Python 3 稍有不同:

import io
import sys

def foo(inStr):
    print ("hi"+inStr)

def test_foo():
    capturedOutput = io.StringIO()                  # Create StringIO object
    sys.stdout = capturedOutput                     #  and redirect stdout.
    foo('test')                                     # Call function.
    sys.stdout = sys.__stdout__                     # Reset redirect.
    print ('Captured', capturedOutput.getvalue())   # Now works as before.

test_foo()

5
在Python 3.8.6中,使用import ioio.StringIO()。对于SocketIO,我收到了AttributeError:module 'io' has no attribute 'SocketIO'的错误。 - Enrique René
@EnriqueRené:我不确定我理解你的评论。自从2017年添加了Python3部分以来,我的答案中一直有io.StringIO()。从来没有提到过SocketIO - paxdiablo
@paxdiablo 我的评论是关于上面 @Alex 的评论,从中我理解到我也可以使用 SocketIO。我尝试了这个建议,但出现了 AttributeError - Enrique René
@qris,我们的店通常不会过于担心这种可能性,因为我们不会区分一个失败和一百万个失败,除了我们花在尝试找出哪个是重要的努力,但几乎总是最早发生的那个。例如,任何带有失败测试的代码都不允许合并到主线。在这种情况下,我们通常会捕获所有可能的异常,关闭处理程序中的捕获,并重新抛出异常。 - undefined
@paxdiablo 但是这个网站不是为了回答你个人的具体问题,而是为了与大家分享知识,其他人可能也会关心。无论如何,在测试中都最好养成自己清理的习惯。 - undefined
显示剩余2条评论

65

这个Python 3.6的答案使用 unittest.mock。它还使用了一个可重复使用的辅助方法assert_stdout,尽管这个辅助方法是特定于正在测试的函数的。

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')

请注意,mock_stdout参数是由unittest.mock.patch装饰器自动传递给assert_stdout方法的。

一个通用的TestStdout类,可能是一个mixin,可以从上面派生出来。

对于使用Python ≥3.4的人来说,contextlib.redirect_stdout也存在,但似乎与unittest.mock.patch没有任何优势。


关于contextlib,它对子进程的输出没有影响。然而,在许多实用脚本中仍然是一种有用的方法,因此不能可靠地使用。 - Wtower
顺便提一下,在assertEqual中,expected是第一个参数。 - Begoodpy
@Begoodpy 不是真的。文档只是简单地提到了“first”和“second”,这就是最终的结论。 - Asclepius
@Begoodpy 它们对此的意见并不重要。此外,你声称 assertEqual(1, 2) 的输出是 Expected :1 Actual :2 是完全错误的。在以前的某些情况下可能是正确的,但现在已经过时了。使用Python 3.11,输出为AssertionError: 1.1 != 2.2备份)。你已经多次提出了虚假的声明,你所说的一切都不可信任。 - Asclepius
@Begoodpy 已收到。答案已更新,提及了Python 3.6,这可能是2017年时的当前版本。 - Asclepius
显示剩余2条评论

24

如果你偶然使用pytest,它内置了输出捕获功能。例如(pytest风格的测试):

def eggs():
    print('eggs')


def test_spam(capsys):
    eggs()
    captured = capsys.readouterr()
    assert captured.out == 'eggs\n'

你也可以在unittest测试类中使用它,但是你需要将fixture对象传递给测试类,例如通过一个自动使用的fixture:

import unittest
import pytest


class TestSpam(unittest.TestCase):

    @pytest.fixture(autouse=True)
    def _pass_fixtures(self, capsys):
        self.capsys = capsys

    def test_eggs(self):
        eggs()
        captured = self.capsys.readouterr()
        self.assertEqual('eggs\n', captured.out)

查看从测试函数中访问捕获的输出以获取更多信息。


12

您也可以使用如下所示的模拟包,这是来自https://realpython.com/lessons/mocking-print-unit-tests的示例。

from mock import patch

def greet(name):
    print('Hello ', name)

@patch('builtins.print')
def test_greet(mock_print):
    # The actual test
    greet('John')
    mock_print.assert_called_with('Hello ', 'John')
    greet('Eric')
    mock_print.assert_called_with('Hello ', 'Eric')

2
我喜欢这个,谢谢,它运行得非常好。 - ofekp

2

答案中的@Acumenus说:

它还使用了一个可重用的辅助方法 assert_stdout,尽管这个辅助方法是特定于被测试的函数的。

粗体部分似乎有一些缺点,因此我会做以下操作:

# extend unittest.TestCase with new functionality
class TestCase(unittest.TestCase):

    def assertStdout(self, expected_output):
        return _AssertStdoutContext(self, expected_output)

    # as a bonus, this syntactical sugar becomes possible:
    def assertPrints(self, *expected_output):
        expected_output = "\n".join(expected_output) + "\n"
        return _AssertStdoutContext(self, expected_output)



class _AssertStdoutContext:

    def __init__(self, testcase, expected):
        self.testcase = testcase
        self.expected = expected
        self.captured = io.StringIO()

    def __enter__(self):
        sys.stdout = self.captured
        return self

    def __exit__(self, exc_type, exc_value, tb):
        sys.stdout = sys.__stdout__
        captured = self.captured.getvalue()
        self.testcase.assertEqual(captured, self.expected)

这使得内容更易于理解和重复使用:
# in a specific test case, the new method(s) can be used
class TestPrint(TestCase):

    def test_print1(self):
        with self.assertStdout("test\n"):
            print("test")

通过使用简单的上下文管理器。(可能还需要将"\n"附加到expected_output,因为print()默认添加换行符。请参见下一个示例...) 此外,这是一种非常好的变体(适用于任意数量的打印!)
    def test_print2(self):
        with self.assertPrints("test1", "test2"):
            print("test1")
            print("test2")

现在是可能的。

1
由于您的类名为TestCase,我猜测您是在继承unittest.TestCase并扩展它,而def test_print(self)则是TestPrintClass类的一部分,其中TestCase是您扩展实现的基类。这样理解是否正确?-- 可能有点废话,但在阅读代码时这是我脑海中浮现的一个问题。 - Matthias dirickx
完全正确。抱歉,在这个例子中漏掉了TestPrintClass。我会添加进去! - NichtJens
我导入了这段代码,但是出现了错误 AttributeError: '_io.TextIOWrapper' object has no attribute 'getvalue' - Joe_Schmoe
奇怪。你使用的是哪个Python版本?文档甚至没有提到该方法是从哪个版本开始包含的:https://docs.python.org/3/library/io.html#io.StringIO.getvalue(对于其他方法有提及)。 - NichtJens

2

您还可以使用contextlib.redirect_stdout捕获方法的标准输出:

import unittest
from contextlib import redirect_stdout
from io import StringIO

class TestMyStuff(unittest.TestCase):
    # ...
    def test_stdout(self):
        with redirect_stdout(StringIO()) as sout:
            my_command_that_prints_to_stdout()
        
        # the stream replacing `stdout` is available outside the `with`
        # you may wish to strip the trailing newline
        retval = sout.getvalue().rstrip('\n')

        # test the string captured from `stdout`
        self.assertEqual(retval, "whatever_retval_should_be")

提供本地范围的解决方案。还可以使用 contextlib.redirect_stderr() 捕获标准错误。


0
另一种变体是依赖于logging模块而不是print()。该模块在文档中也有关于何时使用print的建议

显示命令行脚本或程序的普通用法的控制台输出

PyTest具有内置支持来测试日志消息。


你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

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