How to assert output with nosetest/unittest in python?

65,536

Solution 1

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.

Solution 2

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

Solution 3

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)

Solution 4

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

Solution 5

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!'
Share:
65,536
Pedro Valencia
Author by

Pedro Valencia

Updated on May 13, 2021

Comments

  • Pedro Valencia
    Pedro Valencia about 3 years

    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?

  • Pedro Valencia
    Pedro Valencia over 13 years
    but in this case foo will not be tested... maybe that's a problem
  • Alison R.
    Alison R. over 13 years
    From a testing purist's perspective, perhaps it's a problem. From a practical standpoint, if foo() doesn't do anything but call the print statement, it's probably not a problem.
  • Bryan P
    Bryan P about 11 years
    Note: Under python 3.x the StringIO class must now be imported from the io module. from io import StringIO works in python 2.6+.
  • Silviu
    Silviu over 10 years
    You might want to do a sys.stdout.getvalue().strip() and not cheat comparing with \n :)
  • matiasg
    matiasg about 10 years
    If you use from io import StringIO in python 2, you get a TypeError: unicode argument expected, got 'str' when printing.
  • Andy Hayden
    Andy Hayden over 9 years
    This has worked really well for me in pep8radius. Recently however, I had used this again and get the following error when printing TypeError: unicode argument expected, got 'str' (the type passed to print (str/unicode) is irrelevant).
  • Andy Hayden
    Andy Hayden over 9 years
    Hmmm it may be that in python 2 we want from io import BytesIO as StringIO and in python 3 just from io import StringIO. Seemed to fix the issue in my tests I think.
  • Andy Hayden
    Andy Hayden over 9 years
    Ooop, just to finish, apologies for so many messages. Just to clarify for people finding this: python3 use io.StringIO, python 2 use StringIO.StringIO! Thanks again!
  • Lucretiel
    Lucretiel over 9 years
    Quick note: In python 3.4, you can use the contextlib.redirect_stdout context manager to do this in a way that is exception safe: with redirect_stdout(out):
  • ThorSummoner
    ThorSummoner over 8 years
    You don't need to do saved_stdout = sys.stdout, you always have a magic ref to this at sys.__stdout__, eg, you only need sys.stdout = sys.__stdout__ in your cleanup.
  • Seng Cheong
    Seng Cheong over 8 years
    @ThorSummoner Thanks, this just simplified some of my tests... for scuba which I see you've starred....small world!
  • KobeJohn
    KobeJohn about 8 years
    Good one. Can you include a minimal example since links can disappear and content can change?
  • Palimondo
    Palimondo about 7 years
    Why are all the examples here calling strip() on the unicode returned from StringIO.getvalue()?
  • Rob Kennedy
    Rob Kennedy about 7 years
    @Palimondo, it removes the trailing line break.
  • Palimondo
    Palimondo about 7 years
    @RobKennedy But it also removes newlines that are at the beginning and those at the end and are legitimate part of the output, right?
  • Rob Kennedy
    Rob Kennedy about 7 years
    They're evidently not, in this case, @Palimondo. Note that the test in the question did the same thing.
  • Vedran Šego
    Vedran Šego about 7 years
    Any way to make this work with from sys import stderr, instead of just with import sys?
  • Rob Kennedy
    Rob Kennedy about 7 years
    No, @Vedran. This relies on rebinding the name that belongs to sys. With your import statement, you're creating a local variable named stderr that received a copy of the value in sys.stderr. Changes to one are not reflected in the other.
  • Vedran Šego
    Vedran Šego about 7 years
    @RobKennedy I understand that. My question was: is there a way to capture stderr in that case too?
  • Rob Kennedy
    Rob Kennedy about 7 years
    Sounds like a good question to ask on Stack Overflow, @Vedran.
  • ijoseph
    ijoseph almost 7 years
    Note that this interacts with --nocapture; in particular, if this flag is set, buffered mode will be disabled. So you have the option of either being able to see the output on the terminal, or being able to test that the output is as expected.
  • Lqueryvg
    Lqueryvg over 6 years
    Is it possible to turn this on and off for each test, because this makes debugging very difficult when using something like ipdb.set_trace() ?
  • Sylhare
    Sylhare almost 6 years
    I love this one for Python 3, it is clean!
  • Hypercube
    Hypercube over 5 years
    Great answer! According to the documentation this was added in Python 3.4.
  • Gulzar
    Gulzar about 5 years
    How do I flush the out buffer? out.flush() seems to do nothing (python 2.7)
  • Rob Kennedy
    Rob Kennedy about 5 years
    @Gulzar, StringIO has no buffer to flush. It simply is the buffer. If code has run print 'foo' while the above context manager was active, then foo will be in out.
  • Hubert Grzeskowiak
    Hubert Grzeskowiak about 5 years
    I have a problem with this. When I use the context manager for the second time, it doesn't capture anything. What am I doing wrong?
  • Rob Kennedy
    Rob Kennedy about 5 years
    From that meager description, I couldn't possibly know what you're doing wrong, @Hubert. Have you considered posting a question on Stack Overflow?
  • rbennell
    rbennell about 5 years
    It's 3.4 for redirect_stdout and 3.5 for redirect_stderr. maybe that's where the confusion arose!
  • Edwarric
    Edwarric over 4 years
    The StringIO module is deprecated. Instead from io import StringIO
  • Justin Eyster
    Justin Eyster over 4 years
    This was the only solution on this page that worked for me! Thank you.
  • Adrian W
    Adrian W about 4 years
    redirect_stdout() and redirect_stderr() return their input argument. So, with contextlib.redirect_stdout(StringIO()) as temp_stdout: gives you all in one line. Tested with 3.7.1.
  • MROB
    MROB about 4 years
    It is worth mentioning that the reason for reassigning sys.stdout is the fact that it is write-only (its mode is w), so it is unreadable.
  • StressedBoi69420
    StressedBoi69420 over 2 years
    ModuleNotFoundError: No module named 'StringIO'. Python 3: from io import StringIO
  • MolbOrg
    MolbOrg over 2 years
    sweet, works like charm in 3.7