Search code examples
python-3.xmockingpyteststdoutvalueerror

ValueError while trying to access stdout using pytest and capsys


Trying to test stdout of a local method in an object that verifies users account.

As an example,

class Foo:
    def __init__(self, bar, baz):
        self.bar = bar
        if baz:
            self.something = self.load_something()
        else:
            print('Error initializing')

    def load_something(self):
        return ''

    def make_subprocess_call(self):
        return 'stdout', 'stderr'
    
    def confirm_something(self):
        subprocess_stdout, subprocess_stderr = self.make_subprocess_call()
        if subprocess_stdout:
            print('good output')
        else:
            print('err output', subprocess_stderr)

Then to test:

from Foo_Class_File import Foo
import mock
import pytest

@mock.patch('Foo_Class_File.Foo.load_something', return_value = {'a': 'b'})
@mock.patch('Foo_Class_File.Foo.make_subprocess_call', return_value=('more stdout', 'more_stderr'))
def test_confirm_something(testdir, capsys):
    test_foo = Foo(testdir, True)
    test_foo.confirm_something()
    out, err = capsys.readouterr()
    assert out == 'more stdout'
    assert err == 'more_stderr'

Then running:

python3 -m pytest test_foo_file.py

gives:

============================================================================== FAILURES ===============================================================================
_______________________________________________________________________ test_confirm_something ________________________________________________________________________

testdir = <MagicMock name='make_subprocess_call' id='140533346667632'>, capsys = <MagicMock name='load_something' id='140533346970400'>

    @mock.patch('Foo_Class_File.Foo.load_something', return_value = {'a': 'b'})
    @mock.patch('Foo_Class_File.Foo.make_subprocess_call', return_value=('more stdout', 'more_stderr'))
    def test_confirm_something(testdir, capsys):
        test_foo = Foo(testdir, True)
        test_foo.confirm_something()
>       out, err = capsys.readouterr()
E       ValueError: not enough values to unpack (expected 2, got 0)

blah.py:10: ValueError
------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------
good output
======================================================================= short test summary info =======================================================================
FAILED test_foo_file.py::test_confirm_something - ValueError: not enough values to unpack (expected 2, got 0)
========================================================================== 1 failed in 0.11s ==========================================================================

I'm just really confused because it says that is capturing the stdout?

I think it has to do with instantiating the object and then calling the method, as I have no problem capturing with capsys if there is just the object, but as soon as I try to instantiate it and call a method, what you see above it what happens. I couldn't find a similar case online or in the documentation, not for lack of trying (or perhaps my google foo is weak today).

Apologies if I am doing something silly but at this point I am concerned for the safety of my keyboard and figured I would reach out. Any help/suggestions are appreciated. You can run this test case I posted, it should give you the same error as posted.


Solution

  • Your problem is that you supplied patch decorators, but did not add the respective arguments to the test function. This would be correct:

    @mock.patch('Foo_Class_File.Foo.load_something', return_value = {'a': 'b'})
    @mock.patch('Foo_Class_File.Foo.make_subprocess_call', return_value=('more stdout', 'more_stderr'))
    def test_confirm_something(mocked_call, mocked_load, testdir, capsys):  # note the additional arguments
        test_foo = Foo(testdir, True)
        test_foo.confirm_something()
        out, err = capsys.readouterr()
        assert out == 'good output\n'
        assert err == ''
    

    Or, alternatively:

    @mock.patch('Foo_Class_File.Foo.load_something')
    @mock.patch('Foo_Class_File.Foo.make_subprocess_call')
    def test_confirm_something(mocked_call, mocked_load, testdir, capsys):
        mocked_call.return_value = ('more stdout', 'more_stderr')
        mocked_load.return_value = {'a': 'b'}
        test_foo = Foo(testdir, True)
        ...
    

    What happened in your code is that the fixtures testdir and capsys has been used as the mocked parameters instead (which are expected as the first arguments in the test function), leading to both being a mock object instead of the fixture.

    Side note: it would be nice if pytest gave a warning in this case, something like "You are using a fixture name as a name for a patched object, maybe you forgot to add the patched object argument?"...

    Note that I have replaced the assertions in your example code with the assertions that would actually pass using your code.