Pytest Print or Dump Variables to Console for Debugging
Trying to print(var)
in your pytest tests and wondering why you cannot see the output in your terminal’s standard output? This is because by default pytest captures all output sent to stdout and stderr, i.e., it intercepts the data going into the low-level file descriptors 1
and 2
.
This is a good idea because pytest
can now control the test run output to print a clean and understandable test summary but at the same time it becomes challenging for us when we are trying to debug our test code with print()
(or similar) statements.
How do we solve this? There are a couple of options.
-s or –capture=no option
The easiest way is to run your test with the -s
or --capture=no
option. Say you have this piece of code:
def test_foo():
print('Hello World')
assert True
Then run pytest
with the following options:
$ pytest -s tests/ # or pytest --capture=no tests/
========== test session starts ==========
platform linux -- ...
rootdir: ...
collected 1 item
tests/test_foo.py Hello World
.
========== 1 passed in 0.01s ==========
The Hello World
that you see in the output above is because of print('Hello World')
from inside the test_foo
test function.
Make a Test Fail
With pytest
, if a test or a setup method fails, then all the output to stdout
and stderr
that it captures by default, will be shown in the console with the failure traceback. This is something we could make use of easily. For instance, if you simply fail your test with an assert False
like the following:
def test_foo():
dict = {'hello': 'world'}
print(dict)
assert False
On running the test, you’ll get this output:
$ pytest tests/
========== test session starts ==========
platform linux -- ...
rootdir: ...
collected 1 item
testing/test_foo.py F [100%]
========== FAILURES ==========
__________ test_foo __________
def test_foo():
dict = {'hello': 'world'}
print(dict)
> assert False
E assert False
testing/test_foo.py:4: AssertionError
---------- Captured stdout call ----------
{'hello': 'world'}
========== short test summary info ==========
FAILED testing/test_foo.py::test_foo - assert False
========== 1 failed in 0.02s ==========
Notice the Captured stdout call
section above.
-rP option
Making a passing test fail just to look at the captured output can feel like a hassle. There’s an -r
option that we can pass to pytest
to show extra test summary info. This extra info is configurable by passing a bunch of characters to the option. Basically, if you hit pytest --help
then you’re going to see this section:
-r chars show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. (w)arnings are enabled by default (see --disable-warnings), 'N' can be used to reset the list. (default: 'fE').
If you notice, P
is what we’re looking for. It adds an extra section called PASSES
with those tests that passed but had captured outputs. If you want to see all the passed tests regardless of captured output then the small p
will do the job.
$ pytest -rP tests/
========== test session starts ==========
platform linux -- ...
rootdir: ...
collected 1 item
testing/test_foo.py . [100%]
========== PASSES ==========
_________ test_foo __________
---------- Captured stdout call ----------
Hello World
========== 1 passed in 0.01s ==========
Notice the Captured stdout call
section above. The option usage is like -rP
and if you want to use p
as well then -rpP
.
capsys or capfd
pytest
provides certain built-in fixtures that allows access to the captured stdout
and stderr
output only during a test execution. These fixtures are capsys
, capsysbinary
, capfd
and capfdbinary
. Instead of explaining them myself, I think pytest --fixtures
does a much better job:
capsys
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
...
capsysbinary
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
...
capfd
Enable text capturing of writes to file descriptors ``1`` and ``2``.
...
capfdbinary
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
...
Each fixture has a readouterr()
method that returns the captured output in a namedtuple ((out, err)
) which are either text (capsys
and capfd
) or bytes (capsysbinary
and capfdbinary
) objects.
You still can’t print
the captured output from these fixtures but you can write them out to a file. Let’s see an example:
def test_foo(capsys):
print("Hello World")
out, err = capsys.readouterr()
open('out.txt', 'w').write(out)
open('err.txt', 'w').write(err)
assert True
As you can see, we first specify the capsys
built-in fixture in the function parameter list, then use the readouterr()
method to get the captured output which in this case is Hello World
. The relevant stdout
output is stored in out
and if there were any stderr
output that would go in err
.
We then write any captured output or error into separate files that we can inspect after the test has run.
We can have multiple capsys.readouterr()
calls interleaved amongst multiple prints. Every time it is called, it will return the output captured till that point and the next call will capture output since the previous one only. So basically every call will not give the entire captured output of the function.