63.8 Test Output: Verbosity, Capturing, and Logging
Right, let’s talk about output. Because if you’re running tests and all you get is a blinking cursor followed by a cryptic .F..E.. string, you’re not debugging, you’re deciphering hieroglyphics. We’re better than that. The goal is to get the information you need, precisely when you need it, without the noise. Let’s break down how unittest and pytest handle this, because their philosophies are… different.
The Humble -v Flag: Your First Line of Defense
Forgetfulness is a universal constant. You will run a test, it will fail, and you will immediately forget which test file you were even in. This is why verbosity (-v) is your best friend.
In unittest, running your tests is like getting a terse military report. By default, it’s just dots (.) for passes, F for fails, and E for errors. Useful for a thousand-passing-tests suite, useless for, well, everything else. Add -v and it transforms.
# The "What's even happening?" default
python -m unittest test_module.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s
OK
# The "Ah, human-readable names!" version
python -m unittest test_module.py -v
test_addition (test_module.CalculatorTest) ... ok
test_subtraction (test_module.CalculatorTest) ... ok
test_division_by_zero (test_module.CalculatorTest) ... ERROR
test_multiplication (test_module.CalculatorTest) ... FAIL
pytest is a bit more generous by default, but -v is still crucial. It gives you the full node ID of each test, which is invaluable for quickly copying and re-running a single failing test.
pytest test_module.py -v
========================= test session starts =========================
platform linux -- Python 3.11.0, pytest-7.4.0, pluggy-1.2.0
collected 4 items
test_module.py::test_addition PASSED [ 25%]
test_module.py::test_subtraction PASSED [ 50%]
test_module.py::test_division_by_zero ERROR [ 75%]
test_module.py::test_multiplication FAILED [100%]
See the percentage? That’s pytest being witty and helpful. unittest would never be so emotionally open.
Capturing stdout/stderr: Or, Where Did My Print Statements Go?
Here’s a classic rookie mistake: littering your tests with print("Got here!") statements to debug, only to find they’ve vanished into the ether when the test runs. Both frameworks, sensibly, capture standard output and standard error by default. They only show this output if a test fails. This is a fantastic feature, not a bug. It keeps your passing test output clean.
But sometimes you need to see it for a passing test. Or you need to test that your function actually prints the correct thing. For that, both frameworks have tools.
pytest uses the capsys fixture for this. It’s brilliantly simple.
# test_printing.py
def output_hello():
print("Hello, World!")
def test_output_hello(capsys):
output_hello() # This print is captured
captured = capsys.readouterr()
assert captured.out == "Hello, World!\n" # Note the newline!
unittest handles this with its setUp() and tearDown() methods and the mock.patch functionality, which is more powerful but feels like using a sledgehammer to crack a nut compared to capsys.
Logging: The Professional’s Print Statement
If you’re using the logging module in your application (and you should be), you need to test that too. Capturing log output is a different beast from capturing print statements.
pytest has you covered, again with a fixture: caplog. It’s a thing of beauty.
# test_logging.py
import logging
def log_something():
logging.warning("A cautionary tale")
def test_log_something(caplog):
caplog.set_level(logging.WARNING) # Capture only WARNING and above
log_something()
assert len(caplog.records) == 1
assert caplog.records[0].levelname == "WARNING"
assert "cautionary tale" in caplog.text
unittest requires you to manually add and remove handlers, which is the kind of boilerplate that makes me question my life choices. You can use mock.patch on the logger object, but it’s clunky. This is a clear win for pytest’s design.
The -s Flag: A Nuclear Option
Sometimes, the capturing mechanisms get in the way. Maybe you’re using a debugger, or a third-party library that spews crucial information to stdout in a way the frameworks can’t handle. In these edge cases, you can break the glass and use the -s flag (for both unittest and pytest). This disables all capturing, letting output flow freely to your terminal. It will be messy. You will regret it. But when you’re truly desperate, it’s the only way to see what’s really happening. Use it sparingly, like a fire alarm you only pull when you can actually see flames.