How to assert output with nosetest/unittest in python?

I'm writing tests for a function like next one:

def foo():
print 'hello world!'

So when I want to test this function the code will be like this:

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

But if I run nosetests with -s parameter the test crashes. How can I catch the output with unittest or nose module?

68894 次浏览

If you really want to do this, you can reassign sys.stdout for the duration of the test.

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

If I were writing this code, however, I would prefer to pass an optional out parameter to the foo function.

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

编写测试经常向我们展示一种更好的编写代码的方法。与 Shane 的回答相似,我想提出另一种看待这个问题的方法。您真的想断言您的程序输出了某个字符串,还是仅仅构造了 某种线作为输出?这样就更容易测试了,因为我们可以假设 Pythonprint语句正确地完成了它的工作。

def foo_msg():
return 'hello world'


def foo():
print foo_msg()

Then your test is very simple:

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

Of course, if you really have a need to test your program's actual output, then feel free to disregard. :)

我刚刚开始学习 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')

自从版本2.7以来,您不再需要重新分配 sys.stdout,这是通过 buffer标志提供的。此外,它是鼻测试的默认行为。

下面是一个非缓冲上下文中的失败例子:

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--buffer或在 unittest.main选项中设置缓冲区。 相反的是通过 nosetest标志 --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)

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

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

Use it like this:

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

Furthermore, since the original output state is restored upon exiting the with block, we can set up a second capture block in the same function as the first one, which isn't possible using setup and teardown functions, and gets wordy when writing try-finally blocks manually. That ability came in handy when the goal of a test was to compare the results of two functions relative to each other rather than to some precomputed value.

或者考虑使用 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"

对我来说,很多答案都失败了,因为在 Python3中无法使用 from StringIO import StringIO。下面是基于@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')

在 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!'

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

用法是这样的:

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

N611 x007本体都已经建议使用 unittest.mock,但是这个答案适用于 Acumenus 的,以展示如何轻松地包装 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)

基于这个帖子里所有精彩的答案,这就是我解决它的方法。我想尽可能保留库存。我使用 setUp()增强了单元测试机制来捕获 sys.stdoutsys.stderr,添加了新的断言 API 来根据预期值检查捕获的值,然后在 tearDown(). I did this to keep a similar unit test API as the built-inunittestAPI while still being able to unit test values printed tosys.stdoutorsys.stderr’上恢复 sys.stdoutsys.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)

我喜欢 sorens'对问题和示例代码的直截了当的[回答][1] ,特别是因为我不熟悉诸如补丁/模拟之类的新特性。警笛声没有提出一种方法来使示例代码的 TestStdIO类的自定义断言方法可重用,而不需要使用剪切/粘贴,所以我采用了使 TestStdIO成为一个在其自己的模块中定义的“ Mixin”类的方法(在下面的示例中是 Teststdoutmethods.py)。由于 TestStdIO 中使用的通常由 Unittest TestCase提供的断言方法引用也可以在测试用例类中使用,所以我从他的示例代码中删除了 进口单位检验单位行,并且在类声明中从 Unittest TestCase派生了 TestStdIO,即,

import io
import sys


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

否则,TestStdIO 的代码就是没有最后两个示例用法的 soren 版本。 我在 Ch 中的一个基本示例文本游戏中的一个类的一些简单单元测试用例中使用了 TestStdIO的混合类版本。2张 Kinsley 和 McGugan 的 用 PyGame 开始 Python 游戏编程

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中工作。注意,索伦斯技术捕获 setup ()和 teardown ()调用之间的所有 stdout 输出,因此我将这些输出放在将生成我想检查的特定输出的特定操作周围。我假设我的混合方法是 警笛声希望通用重用的方法,但是我想知道是否有人有不同的建议。谢谢。 [1]: https://stackoverflow.com/a/62429695/7386731

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/