如何在python中使用鼻子测试/单元测试来断言输出?


114

我正在为下一个功能编写测试:

def foo():
    print 'hello world!'

因此,当我要测试此功能时,代码将如下所示:

import sys
from foomodule import foo
def test_foo():
    foo()
    output = sys.stdout.getline().strip() # because stdout is an StringIO instance
    assert output == 'hello world!'

但是,如果我使用-s参数运行鼻子测试,则测试将崩溃。如何使用unittest或鼻子模块捕获输出?


Answers:


124

我使用此上下文管理器捕获输出。通过临时替换,它最终使用了与其他一些答案相同的技术sys.stdout。我更喜欢上下文管理器,因为它将所有簿记功能都包装到一个函数中,因此,我不必重新编写任何最终代码,也不必为此编写设置和拆卸功能。

import sys
from contextlib import contextmanager
from StringIO import StringIO

@contextmanager
def captured_output():
    new_out, new_err = StringIO(), StringIO()
    old_out, old_err = sys.stdout, sys.stderr
    try:
        sys.stdout, sys.stderr = new_out, new_err
        yield sys.stdout, sys.stderr
    finally:
        sys.stdout, sys.stderr = old_out, old_err

像这样使用它:

with captured_output() as (out, err):
    foo()
# This can go inside or outside the `with` block
output = out.getvalue().strip()
self.assertEqual(output, 'hello world!')

此外,由于退出with块后将恢复原始输出状态,因此我们可以使用与第一个捕获块相同的功能来设置第二个捕获块,使用设置和拆卸功能是不可能的,并且在最终尝试写入时会变得很冗长手动阻止。当测试的目的是将两个函数的结果相互比较而不是与某个预先计算的值进行比较时,该功能就派上用场了。


pep8radius中,这对我来说确实非常有效。但是最近,我再次使用了它,并且在打印时收到以下错误TypeError: unicode argument expected, got 'str'(传递给print的类型(str / unicode)不相关)。
安迪·海登

9
可能是在python 2中我们想要的,from io import BytesIO as StringIO而在python 3中只是from io import StringIO。我认为是为了在我的测试中解决问题。
安迪·海登

4
抱歉,对于这么多消息,我们深表歉意。只是为了向发现这一点的人澄清:python3使用io.StringIO,python 2使用StringIO.StringIO!再次感谢!
安迪·海登

为什么要在这里呼吁所有的例子strip()unicode从返回StringIO.getvalue()
Palimondo

1
不,@ Vedran。这取决于重新绑定属于的名称sys。使用import语句,您将创建一个名为的本地变量stderr,该变量在中接收了值的副本sys.stderr。对一个的更改不会反映在另一个中。
罗伯·肯尼迪

60

如果确实要执行此操作,则可以在测试期间重新分配sys.stdout。

def test_foo():
    import sys
    from foomodule import foo
    from StringIO import StringIO

    saved_stdout = sys.stdout
    try:
        out = StringIO()
        sys.stdout = out
        foo()
        output = out.getvalue().strip()
        assert output == 'hello world!'
    finally:
        sys.stdout = saved_stdout

但是,如果我正在编写此代码,则希望将可选out参数传递给该foo函数。

def foo(out=sys.stdout):
    out.write("hello, world!")

然后测试要简单得多:

def test_foo():
    from foomodule import foo
    from StringIO import StringIO

    out = StringIO()
    foo(out=out)
    output = out.getvalue().strip()
    assert output == 'hello world!'

11
注意:在python 3.x下,StringIO该类现在必须从io模块中导入。from io import StringIO适用于python 2.6+。
Bryan P

2
如果from io import StringIO在python 2中使用,则TypeError: unicode argument expected, got 'str'在打印时会显示。
matiasg 2014年

9
快速说明:在python 3.4中,您可以使用contextlib.redirect_stdout上下文管理器以异常安全的方式执行此操作:with redirect_stdout(out):
Lucretiel 2014年

2
您不需要这样做saved_stdout = sys.stdout,在处总是对此有个神奇的参考sys.__stdout__,例如,您只需要sys.stdout = sys.__stdout__清理即可。
ThorSummoner

@ThorSummoner谢谢,这只是简化了我的一些测试……对于水肺潜水,我看到你已经出演了……小世界!
乔纳森·莱因哈特

48

从2.7版开始,您不再需要重新分配sys.stdout,这是通过bufferflag提供的。而且,这是鼻子测试的默认行为。

这是在非缓冲上下文中失败的示例:

import sys
import unittest

def foo():
    print 'hello world!'

class Case(unittest.TestCase):
    def test_foo(self):
        foo()
        if not hasattr(sys.stdout, "getvalue"):
            self.fail("need to run in buffered mode")
        output = sys.stdout.getvalue().strip() # because stdout is an StringIO instance
        self.assertEquals(output,'hello world!')

您可以设置缓冲区通过unit2命令行标志-b--bufferunittest.main选择。相反的是通过nosetestflag 实现的--nocapture

if __name__=="__main__":   
    assert not hasattr(sys.stdout, "getvalue")
    unittest.main(module=__name__, buffer=True, exit=False)
    #.
    #----------------------------------------------------------------------
    #Ran 1 test in 0.000s
    #
    #OK
    assert not hasattr(sys.stdout, "getvalue")

    unittest.main(module=__name__, buffer=False)
    #hello world!
    #F
    #======================================================================
    #FAIL: test_foo (__main__.Case)
    #----------------------------------------------------------------------
    #Traceback (most recent call last):
    #  File "test_stdout.py", line 15, in test_foo
    #    self.fail("need to run in buffered mode")
    #AssertionError: need to run in buffered mode
    #
    #----------------------------------------------------------------------
    #Ran 1 test in 0.002s
    #
    #FAILED (failures=1)

请注意,这与--nocapture; 相互作用。特别是,如果设置了此标志,则将禁用缓冲模式。所以,你要么能够看到终端上的输出,的选项或者是预期的输出能够测试。
ijoseph

1
是否可以为每个测试将其打开和关闭,因为这在使用ipdb.set_trace()之类的工具时使调试变得非常困难?
Lqueryvg

33

对于我来说,这些答案很多都失败了,因为您无法from StringIO import StringIO使用Python3。这是基于@naxa的注释和Python Cookbook的最小工作片段。

from io import StringIO
from unittest.mock import patch

with patch('sys.stdout', new=StringIO()) as fakeOutput:
    print('hello world')
    self.assertEqual(fakeOutput.getvalue().strip(), 'hello world')

3
我喜欢Python 3的这个,很干净!
Sylhare '18

1
这是此页面上唯一对我有用的解决方案!谢谢。
贾斯汀·艾斯特

24

在python 3.5中,您可以使用contextlib.redirect_stdout()StringIO()。这是对代码的修改

import contextlib
from io import StringIO
from foomodule import foo

def test_foo():
    temp_stdout = StringIO()
    with contextlib.redirect_stdout(temp_stdout):
        foo()
    output = temp_stdout.getvalue().strip()
    assert output == 'hello world!'

好答案!根据文档,这是在Python 3.4中添加的。
Hypercube

redirect_stdout为3.4,redirect_stderr为3.5。也许就是在这里引起混乱!
rbennell

redirect_stdout()redirect_stderr()返回其输入参数。因此,with contextlib.redirect_stdout(StringIO()) as temp_stdout:将您全都集中在一起。经过3.7.1测试。
艾德里安W

17

我只是在学习Python,并且发现自己遇到了与上述类似的问题,并且对带有输出的方法进行了单元测试。我上面对foo模块的传递单元测试最终看起来像这样:

import sys
import unittest
from foo import foo
from StringIO import StringIO

class FooTest (unittest.TestCase):
    def setUp(self):
        self.held, sys.stdout = sys.stdout, StringIO()

    def test_foo(self):
        foo()
        self.assertEqual(sys.stdout.getvalue(),'hello world!\n')

5
您可能想要做一个,sys.stdout.getvalue().strip()而不是作弊\n:)
Silviu 2014年

不建议使用StringIO模块。相反from io import StringIO
Edwarric

10

编写测试通常会向我们展示一种更好的编写代码的方法。与Shane的答案类似,我想提出另一种看待这个问题的方式。您是否真的要断言您的程序输出了某个字符串,还是只是构造了一个用于输出的字符串?这变得更容易测试,因为我们可能可以假设Python print语句正确完成了工作。

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

然后您的测试非常简单:

def test_foo_msg():
    assert 'hello world' == foo_msg()

当然,如果您确实需要测试程序的实际输出,则可以忽略。:)


1
但是在这种情况下,foo将不会经过测试...也许这是一个问题
Pedro Valencia

5
从测试主义者的角度来看,也许是个问题。从实际的角度来看,如果foo()除了调用print语句之外什么也没做,那可能不是问题。
艾莉森·R.2010年

5

基于Rob Kennedy的回答,我编写了一个基于类的上下文管理器版本来缓冲输出。

用法就像:

with OutputBuffer() as bf:
    print('hello world')
assert bf.out == 'hello world\n'

这是实现:

from io import StringIO
import sys


class OutputBuffer(object):

    def __init__(self):
        self.stdout = StringIO()
        self.stderr = StringIO()

    def __enter__(self):
        self.original_stdout, self.original_stderr = sys.stdout, sys.stderr
        sys.stdout, sys.stderr = self.stdout, self.stderr
        return self

    def __exit__(self, exception_type, exception, traceback):
        sys.stdout, sys.stderr = self.original_stdout, self.original_stderr

    @property
    def out(self):
        return self.stdout.getvalue()

    @property
    def err(self):
        return self.stderr.getvalue()

2

或考虑使用pytest,它具有内置的断言stdout和stderr支持。查看文件

def test_myoutput(capsys): # or use "capfd" for fd-level
    print("hello")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"
    print("next")
    captured = capsys.readouterr()
    assert captured.out == "next\n"

好一个 您能否提供一个最小的示例,因为链接可能会消失,内容可能会更改?
科比约翰

2

无论n611x007本体使用已经建议unittest.mock,但这个答案适应阿卡门斯的向您展示如何轻松地包裹unittest.TestCase方法与嘲笑互动stdout

import io
import unittest
import unittest.mock

msg = "Hello World!"


# function we will be testing
def foo():
    print(msg, end="")


# create a decorator which wraps a TestCase method and pass it a mocked
# stdout object
mock_stdout = unittest.mock.patch('sys.stdout', new_callable=io.StringIO)


class MyTests(unittest.TestCase):

    @mock_stdout
    def test_foo(self, stdout):
        # run the function whose output we want to test
        foo()
        # get its output from the mocked stdout
        actual = stdout.getvalue()
        expected = msg
        self.assertEqual(actual, expected)

0

基于此线程中所有出色的答案,这就是我解决的方法。我想尽可能地保留它。我使用增强的单元测试机制setUp(),以捕获sys.stdoutsys.stderr,增加了新的API断言检查捕获的值对一个预期值,然后还原sys.stdoutsys.stderrtearDown(). I did this to keep a similar unit test API as the built-in单元测试API while still being able to unit test values printed tosys.stdout的orsys.stderr`。

import io
import sys
import unittest


class TestStdout(unittest.TestCase):

    # before each test, capture the sys.stdout and sys.stderr
    def setUp(self):
        self.test_out = io.StringIO()
        self.test_err = io.StringIO()
        self.original_output = sys.stdout
        self.original_err = sys.stderr
        sys.stdout = self.test_out
        sys.stderr = self.test_err

    # restore sys.stdout and sys.stderr after each test
    def tearDown(self):
        sys.stdout = self.original_output
        sys.stderr = self.original_err

    # assert that sys.stdout would be equal to expected value
    def assertStdoutEquals(self, value):
        self.assertEqual(self.test_out.getvalue().strip(), value)

    # assert that sys.stdout would not be equal to expected value
    def assertStdoutNotEquals(self, value):
        self.assertNotEqual(self.test_out.getvalue().strip(), value)

    # assert that sys.stderr would be equal to expected value
    def assertStderrEquals(self, value):
        self.assertEqual(self.test_err.getvalue().strip(), value)

    # assert that sys.stderr would not be equal to expected value
    def assertStderrNotEquals(self, value):
        self.assertNotEqual(self.test_err.getvalue().strip(), value)

    # example of unit test that can capture the printed output
    def test_print_good(self):
        print("------")

        # use assertStdoutEquals(value) to test if your
        # printed value matches your expected `value`
        self.assertStdoutEquals("------")

    # fails the test, expected different from actual!
    def test_print_bad(self):
        print("@=@=")
        self.assertStdoutEquals("@-@-")


if __name__ == '__main__':
    unittest.main()

运行单元测试时,输出为:

$ python3 -m unittest -v tests/print_test.py
test_print_bad (tests.print_test.TestStdout) ... FAIL
test_print_good (tests.print_test.TestStdout) ... ok

======================================================================
FAIL: test_print_bad (tests.print_test.TestStdout)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tests/print_test.py", line 51, in test_print_bad
    self.assertStdoutEquals("@-@-")
  File "/tests/print_test.py", line 24, in assertStdoutEquals
    self.assertEqual(self.test_out.getvalue().strip(), value)
AssertionError: '@=@=' != '@-@-'
- @=@=
+ @-@-


----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
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.