问题:如何在python中使用鼻子测试/单元测试来断言输出?
我正在为下一个功能编写测试:
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或鼻子模块捕获输出?
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?
回答 0
我使用此上下文管理器捕获输出。通过临时替换,它最终使用了与其他一些答案相同的技术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
块后将恢复原始输出状态,因此我们可以使用与第一个捕获块相同的功能来设置第二个捕获块,使用设置和拆卸功能是不可能的,并且在最终尝试写入时会变得很冗长手动阻止。当测试的目的是将两个函数的结果相互比较而不是与某个预先计算的值进行比较时,该功能就派上用场了。
I use this context manager to capture output. It ultimately uses the same technique as some of the other answers by temporarily replacing sys.stdout
. I prefer the context manager because it wraps all the bookkeeping into a single function, so I don’t have to re-write any try-finally code, and I don’t have to write setup and teardown functions just for this.
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.
回答 1
如果确实要执行此操作,则可以在测试期间重新分配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!'
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!")
Then the test is much simpler:
def test_foo():
from foomodule import foo
from StringIO import StringIO
out = StringIO()
foo(out=out)
output = out.getvalue().strip()
assert output == 'hello world!'
回答 2
从2.7版开始,您不再需要重新分配sys.stdout
,这是通过buffer
flag提供的。而且,这是鼻子测试的默认行为。
这是在非缓冲上下文中失败的示例:
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
flag 实现的--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)
Since version 2.7, you do not need anymore to reassign sys.stdout
, this is provided through buffer
flag. Moreover, it is the default behavior of nosetest.
Here is a sample failing in non buffered context:
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!')
You can set buffer through unit2
command line flag -b
, --buffer
or in unittest.main
options.
The opposite is achieved through nosetest
flag --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)
回答 3
对于我来说,这些答案很多都失败了,因为您无法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')
A lot of these answers failed for me because you can’t from StringIO import StringIO
in Python 3. Here’s a minimum working snippet based on @naxa’s comment and the 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')
回答 4
在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!'
In python 3.5 you can use contextlib.redirect_stdout()
and StringIO()
. Here’s the modification to your code
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!'
回答 5
我只是在学习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')
I’m only just learning Python and found myself struggling with a similar problem to the one above with unit tests for methods with output. My passing unit test for foo module above has ended up looking like this:
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')
回答 6
编写测试通常会向我们展示一种更好的编写代码的方法。与Shane的答案类似,我想提出另一种看待这个问题的方式。您是否真的要断言您的程序输出了某个字符串,还是只是构造了一个用于输出的字符串?这变得更容易测试,因为我们可能可以假设Python print
语句正确完成了工作。
def foo_msg():
return 'hello world'
def foo():
print foo_msg()
然后您的测试非常简单:
def test_foo_msg():
assert 'hello world' == foo_msg()
当然,如果您确实需要测试程序的实际输出,则可以忽略。:)
Writing tests often shows us a better way to write our code. Similar to Shane’s answer, I’d like to suggest yet another way of looking at this. Do you really want to assert that your program outputted a certain string, or just that it constructed a certain string for output? This becomes easier to test, since we can probably assume that the Python print
statement does its job correctly.
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. :)
回答 7
基于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()
Based on Rob Kennedy’s answer, I wrote a class-based version of the context manager to buffer the output.
Usage is like:
with OutputBuffer() as bf:
print('hello world')
assert bf.out == 'hello world\n'
Here’s the implementation:
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()
回答 8
或考虑使用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"
Or consider using pytest
, it has built-in support for asserting stdout and stderr. See docs
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"
回答 9
无论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)
Both n611x007 and Noumenon already suggested using unittest.mock
, but this answer adapts Acumenus’s to show how you can easily wrap unittest.TestCase
methods to interact with a mocked 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)
回答 10
基于此线程中所有出色的答案,这就是我解决的方法。我想尽可能地保留它。我使用增强的单元测试机制setUp()
,以捕获sys.stdout
和sys.stderr
,增加了新的API断言检查捕获的值对一个预期值,然后还原sys.stdout
并sys.stderr
在tearDown(). I did this to keep a similar unit test API as the built-in
单元测试API while still being able to unit test values printed to
sys.stdout的or
sys.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)
Building on all the awesome answers in this thread, this is how I solved it. I wanted to keep it as stock as possible. I augmented the unit test mechanism using setUp()
to capture sys.stdout
and sys.stderr
, added new assert APIs to check the captured values against an expected value and then restore sys.stdout
and sys.stderr
upon tearDown(). I did this to keep a similar unit test API as the built-in
unittestAPI while still being able to unit test values printed to
sys.stdoutor
sys.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()
When the unit test is run, the output is:
$ 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)