122

我正在为下一个函数编写测试:

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 或 nose 模块捕获输出?

4

13 回答 13

131

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

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块时会恢复原始输出状态,我们可以在与第一个相同的函数中设置第二个捕获块,这是使用 setup 和 teardown 函数无法实现的,并且在编写 try-finally 时会变得冗长手动阻止。当测试的目标是比较两个函数的结果,而不是与某个预先计算的值进行比较时,这种能力就派上用场了。

于 2013-07-31T22:16:00.643 回答
65

如果您真的想这样做,您可以在测试期间重新分配 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!'
于 2010-11-18T22:28:52.870 回答
48

从 2.7 版开始,您不再需要 reassign sys.stdout,这是通过bufferflag提供的。此外,它是nosetest 的默认行为。

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

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)
于 2012-10-02T00:29:47.617 回答
39

很多这些答案对我来说都失败了,因为你不能from StringIO import StringIO在 Python 3 中。这是基于@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')
于 2015-07-08T00:07:54.700 回答
30

在 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!'
于 2016-09-04T19:20:14.507 回答
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')
于 2011-10-04T22:33:04.990 回答
10

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

def foo_msg():
    return 'hello world'

def foo():
    print foo_msg()

那么你的测试就很简单了:

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

当然,如果您确实需要测试程序的实际输出,请随意忽略。:)

于 2010-11-19T01:02:00.370 回答
5

n611x007和Noumenon已经建议使用,但是这个答案改编了 Acumenus 的,以展示如何轻松包装方法以与模拟的.unittest.mockunittest.TestCasestdout

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)
于 2020-05-03T21:42:08.720 回答
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()
于 2017-10-22T21:07:03.403 回答
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"
于 2015-06-23T14:55:43.863 回答
1

Unittest 现在附带一个上下文管理器(Python 3.7,但也可能是更早的版本)。你可以这样做:

# example.py

import logging

def method_with_logging():
    logging.info("Hello, World!")

然后在你的单元测试中:

# test.py

from unittest import TestCase
from example import method_with_logging

class TestExample(TestCase):
    def test_logging(self):
        with self.assertLogs() as captured:
            method_with_logging()
        self.assertEqual(len(captured.records), 1) # check that there is only one log message
        self.assertEqual(captured.records[0].getMessage(), "Hello, World!") # and it is the proper one

取自https://pythonin1minute.com/how-to-test-logging-in-python/

于 2021-05-13T04:20:24.793 回答
0

基于此线程中所有令人敬畏的答案,这就是我解决它的方法。我想尽可能地保留它。我增强了setUp()用于捕获sys.stdout和的单元测试机制sys.stderr,添加了新的断言 API 以根据预期值检查捕获的值,然后在unittest sys.stdout sys.stderr时恢复sys.stdout和。sys.stderrtearDown(). I did this to keep a similar unit test API as the built-inAPI while still being able to unit test values printed toor

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)
于 2020-06-17T13:04:11.080 回答
0

我喜欢sorens对问题和示例代码的直截了当的 [Answer][1],特别是因为我不熟悉补丁/模拟等新功能。sorens没有建议一种方法来使示例代码的TestStdIO类的自定义断言方法可重用,而无需借助剪切/粘贴,因此我采用了使TestStdIO成为在其自己的模块中定义的“mixin”类的方法(teststdoutmethods.py在下面的例子)。由于 TestStdIO 中使用的通常unittest.TestCase提供的断言方法引用也将在测试用例类中可用,因此我从他的示例代码中删除了import unittest行以及TestStdIO的派生来自类声明中的unittest.TestCase,即

import io
import sys

class TestStdIO(object):
    def setUp(self):
        ...

否则,TestStdIO 的代码与 sorens 的版本一样,没有最后的两个示例用法。我在 Ch 中的一个基本示例文本游戏中的一个类的一些简单的单元测试测试用例中使用了TestStdIO的这个 mixin 类版本。Kinsley 和 McGugan使用 PyGame 开始 Python 游戏编程的2 篇,例如

import unittest
from teststdoutmethods import TestStdIO   # sorens' TestStdIO as a mixin.
from tank import Tank  # From Beginning Python Game Programming with PyGame.

class Test_Tank_fire(TestStdIO, unittest.TestCase):   # Note multiple inheritance.

    def test_Tank_fire_wAmmo(self):
        oTank1 = Tank('Bill', 5, 100)
        oTank2 = Tank('Jim', 5, 100)

        self.setUp()
        oTank1.fire_at(oTank2)

        self.assertStdoutEquals("Bill fires on Jim\nJim is hit!")
        self.assertEqual(str(oTank1), 'Bill (100 Armor, 4 Ammo)', 'fire_at shooter attribute results incorrect')
        self.assertTrue(str(oTank2) == 'Jim (80 Armor, 5 Ammo)', 'fire_at target attribute results incorrect')

        self.tearDown()

    def test_Tank_fire_woAmmo(self):
        oTank1 = Tank('Bill', 5, 100)
        oTank2 = Tank('Jim', 5, 100)

        # Use up 5 allotted shots.
        for n in range(5):
            oTank1.fire_at(oTank2)

        self.setUp()
        # Try one more.
        oTank1.fire_at(oTank2)

        self.assertStdoutEquals("Bill has no shells!")

        self.tearDown()
    
    def test_Tank_explode(self):
        oTank1 = Tank('Bill', 5, 100)
        oTank2 = Tank('Jim', 5, 100)

        # Use up 4 shots.
        for n in range(4):
            oTank1.fire_at(oTank2)

        self.setUp()
        # Fifth shot should finish the target.
        oTank1.fire_at(oTank2)

        self.assertStdoutEquals("Bill fires on Jim\nJim is hit!\nJim explodes!")
        self.tearDown()

        self.assertTrue(str(oTank2) == 'Jim (DEAD)', 'fire_at target __str__ incorrect when Dead')

测试用例(成功和失败)在 Python 3.7 中运行。请注意,sorens 的技术捕获了 setup() 和 teardown() 调用之间的所有 stdout 输出,因此我将它们放置在将生成我想要检查的特定输出的特定操作周围。我认为我的 mixin 方法是sorens打算用于一般重用的方法,但我想知道是否有人有不同的建议。谢谢。[1]:https ://stackoverflow.com/a/62429695/7386731

于 2020-11-23T20:59:51.943 回答