That's why I love pytest.
You just `assert` an expression, and on failure it'll break down each sub-expression and show whatever it yielded, plus by default it captures all output (stdout and stderr) and prints it out only on failure (so logging and other console output is not painfully annoying while testing) and the fixtures systems is just fantastic and parameterised tests make for much clearer data-driven (/table-driven) tests and the tests selection at the CLI is awesome.
And all the complexity is in the test framework and runner, all you do is write
def test_thing(a_fixture):
assert foo(bar) == a_fixture.some_thing()
or whatever.