I have some unittest-based code that currently looks like this:
class TestMain(TestCase):
def run_prog(self, args):
with TemporaryFile() as stdout:
old_stdout = sys.stdout
try:
main.main()
stdout.seek(0)
stdout_data = stdout.read()
finally:
sys.stdout = old_stdout
return stdout_data
def test_one(self):
out = self.run_prog(...)
def test_two(self):
out = self.run_prog(...)
def test_three(self):
out = self.run_prog(...)
run_prog invokes the "main" program under test and manually captures its stdout.
I'm in the process of converting this project to pytest, but this last piece has pushed the limits of my understanding of pytest fixtures.
I understand that pytest has full support for capturing stdout/stderr and I would like to leverage this.
The problem is, their examples work on a test-function level:
def test_myoutput(capfd):
do_something
captured = capsys.readouterr()
assert captured.out == "hello\n"
assert captured.err == "world\n"
In my case, run_prog is used 42 times, so I'm trying to use the fixture starting at run_prog -- the calling functions ideally don't need to bother with capsys/capfd.
Is there a way to "invoke" the fixture from my run_prog helper? Or do I need to add capfd to all 42 tests and pass it to run_prog?
You can define an autouse fixture that will store the CaptureFixture object (returned by the capsys fixture) as an instance property:
class TestMain(TestCase):
#pytest.fixture(autouse=True)
def inject_capsys(self, capsys):
self._capsys = capsys
def run_prog(self, args):
main.main()
return self._capsys.out
def test_out(self):
assert self.run_prog('spam') == 'eggs'
The TestMain.inject_capsys fixture will be rerun for each test, guaranteeing the test isolation (no output from test_one will be leaked in test_two etc).
Here's a slight variation on hoefling's answer that gives a little more control over the scope of the capsys fixture.
It uses request.getfixturevalue() to retrieve the fixture at function invocation time:
import pytest
import sys
class TestMain:
#pytest.fixture(autouse=True)
def inject_request(self, request):
self.request = request
def run_prog(self, message):
capfd = self.request.getfixturevalue('capfd')
sys.stdout.write(message)
captured = capfd.readouterr()
assert captured.out == message
def test_one(self):
self.run_prog("Hello world!")
Related
I wrote a Thing class that does logging using loguru. At the bottom of the class file I add a handler to the logger. This is what thing.py looks like.
from loguru import logger
class Thing:
def __init__(self):
logger.info("Thing created")
def __enter__(self):
logger.info("Thing entered")
return self
def __exit__(self, exc_type, exc_value, traceback):
logger.info("Thing exited")
logger.add("thing.log")
if __name__ == "__main__":
with Thing() as thing:
logger.info("In with block")
This works fine and it logs to thing.log as expected. What I would like to achieve is that it does not add the handler to thing.log when running tests.
This is my test file:
import pytest
from loguru import logger
from thing import Thing
#pytest.fixture
def thing(mocker):
mocker.patch("thing.logger", logger)
with Thing() as thing:
yield thing
def test_thing(thing, mocker):
mocker.patch("thing.logger", logger)
logger.info("In test")
assert isinstance(thing, Thing)
Now this test passes, but the logs are still written to thing.log (instead to only stdout, which is the default in for a loguru.logger).
How do I make sure that it only logs to the basic loguru.logger when running pytest?
What I tried:
Using monkeypatch instead of using mocker: monkeypatch.setattr("thing.logger", logger)
Patching in only one place (either in the fixture or in the test function)
Patching without replacement: mocker.patch("thing.logger") (so without a replacement logger)
Remove logger.add("thing.log") from thing.py!
You can either specify (as said in the docs) that you want to log to stdout: logger.add(sys.stdout) or just leave it out because the default for loguru.logger is in fact stdout!
The example provided in their docs:
logger.add(sys.stdout, format="{time} - {level} - {message}", filter="sub.module")
EDIT:
if __name__ == "__main__":
logger.add("thing.log")
if __name__ == "__main__":
with Thing() as thing:
#...
Now the logger will log to thing.log when the module is executed directly, but it will NOT add the file handler when the module is imported by another module (e.g. a test file).
Or you can use logger.remove(0) to stop logging when calling thing(mocker)!
Assume we have:
#pytest.fixture()
def setup():
print('All set up!')
return True
def foo(setup):
print('I am using a fixture to set things up')
setup_done=setup
I'm looking for a way to get to know caller function name (in this case: foo) from within setup fixture.
So far I have tried:
import inspect
#pytest.fixture()
def setup():
daddy_function_name = inspect.stack()[1][3]
print(daddy_function_name)
print('All set up!')
return True
But what gets printed is: call_fixture_func
How do I get foo from printing daddy_function_name?
You can use the built-in request fixture in your own fixture:
The request fixture is a special fixture providing information of the requesting test function.
Its node attribute is the
Underlying collection node (depends on current request scope).
import pytest
#pytest.fixture()
def setup(request):
return request.node.name
def test_foo(setup):
assert setup == "test_foo"
I have a module loaders with class "Loader" with class method "load". During test, I want to append some additional steps to "Loader.load" to account for test specific data post-processing, so essentially overriding it. How do I properly do that?
I tried creating a mock class which inherits Loader and use monkeypatch.setattr("loaders.Loader", mock_loader), but this only works when I run one single test, but not when I run all tests.
loaders.py
class Loader:
def load():
# do something
return data
test.py
from loaders import Loader
class MockLoader(Loader):
def load():
data = super().load()
# do something to data
return data
def test_loader_special1(monkeypatch):
monkeypatch.setattr("loaders.Loader", MockLoader)
#run test logic 1
def test_loader_special2(monkeypatch):
monkeypatch.setattr("loaders.Loader", MockLoader)
#run test logic 2
Use patch or patch.object either via the builtin library unittest or the external library pytest-mock :
patch() acts as a function decorator, class decorator or a context
manager. Inside the body of the function or with statement, the target
is patched with a new object.
Where it is explicitly documented that the patch is only applicable per test:
When the function/with statement exits the patch is undone.
The patch will be used to wrap around your real implementation so that you can perform any necessary steps before and/or after calling it.
loaders.py
class Loader:
def load(self):
print("Real load() called")
return "real"
test_loaders.py
import pytest
from unittest.mock import patch
from loaders import Loader
#pytest.fixture
def mock_load(mocker):
real_func = Loader.load
def mock_func(self, *args, **kwargs):
print("Mock load() called")
data = real_func(self, *args, **kwargs)
data += " and mock"
return data
# Option 1: Using pytest-mock + new
mocker.patch.object(Loader, 'load', new=mock_func)
"""
Alternative ways of doing Option 1. All would just work the same.
# Option 2: Using pytest-mock + side_effect
mocker.patch.object(Loader, 'load', side_effect=mock_func, autospec=True)
# Option 3: Using unittest + new
with patch.object(Loader, 'load', new=mock_func):
yield
# Option 4: Using unittest + new
with patch.object(Loader, 'load', side_effect=mock_func, autospec=True):
yield
"""
def test_loader_special1(mock_load):
data = Loader().load()
print(f"{data=}")
assert data == "real and mock"
def test_loader_special2(mock_load):
data = Loader().load()
print(f"{data=}")
assert data == "real and mock"
def test_loader_special3():
data = Loader().load()
print(f"{data=}")
assert data == "real"
def test_loader_special4(mock_load):
data = Loader().load()
print(f"{data=}")
assert data == "real and mock"
Output:
$ pytest test_loaders.py -q -rP
.... [100%]
================================================================================================= PASSES ==================================================================================================
__________________________________________________________________________________________ test_loader_special1 ___________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Mock load() called
Real load() called
data='real and mock'
__________________________________________________________________________________________ test_loader_special2 ___________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Mock load() called
Real load() called
data='real and mock'
__________________________________________________________________________________________ test_loader_special3 ___________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Real load() called
data='real'
__________________________________________________________________________________________ test_loader_special4 ___________________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Mock load() called
Real load() called
data='real and mock'
4 passed in 0.01s
Modifying the answer https://stackoverflow.com/a/68675071/14536215 to use monkeypatch:
#pytest.fixture
def mock_load(monkeypatch):
real_func = Loader.load
def mock_func(self, *args, **kwargs):
print("Mock load() called")
data = real_func(self, *args, **kwargs)
data += " and mock"
return data
monkeypatch.setattr(Loader, "load", mock_func)
You can then mark the fixture to be loaded for all tests in the testing module with:
#pytest.fixture(autouse=True)
or mark the tests to use specified fixtures with:
#pytest.mark.usefixtures("mock_load")
def test_loader_special1():
...
Edit:
If you want to mock the whole class, you need to mock it before you import the class for it to take effect, or you could just import the module so you don't have the import statements sprinkled around:
import loaders
class MockLoader(loaders.Loader):
def load(self):
data = super().load()
data += " and mock"
return data
def test_loader_special1(monkeypatch):
monkeypatch.setattr("loaders.Loader", MockLoader)
data = loaders.Loader().load()
print(f"{data=}")
assert data == "real and mock"
I am using a pytest fixture to mock up command-line arguments for testing a script. This way the arguments shared by each test function would only need to be declared in one place. I'm also trying to use pytest's capsys to capture output printed by the script. Consider the following silly example.
from __future__ import print_function
import pytest
import othermod
from sys import stdout
#pytest.fixture
def shared_args():
args = type('', (), {})()
args.out = stdout
args.prefix = 'dude:'
return args
def otherfunction(message, prefix, stream):
print(prefix, message, file=stream)
def test_dudesweet(shared_args, capsys):
otherfunction('sweet', shared_args.prefix, shared_args.out)
out, err = capsys.readouterr()
assert out == 'dude: sweet\n'
Here, capsys does not capture sys.stderr properly. If I move from sys import stdout and args.out = stdout directly into the test function, things work as expected. But this makes things much messier, as I have to re-declare these statements for each test. Am I doing something wrong? Can I use capsys with fixtures?
Fixture is invoked before test is run. In your example, shared_args fixture is reading stdout before otherfunction can write anything to stdout.
One way to fix your problem is to make your fixture return a function which can do what you want it to do. You can scope the fixture according to your use case.
from __future__ import print_function
import pytest
from sys import stdout
import os
#pytest.fixture(scope='function')
def shared_args():
def args_func():
args = type('', (), {})()
args.out = stdout
args.prefix = 'dude:'
return args
return args_func
def otherfunction(message, prefix, stream):
print(prefix, message, file=stream)
def test_dudesweet(shared_args, capsys):
prefix, out = shared_args().prefix, shared_args().out
otherfunction('sweet', prefix, out)
out, err = capsys.readouterr()
assert out == 'dude: sweet\n'
You are not using capsys.readouterr() correctly. See the correct usage of capsys here: https://stackoverflow.com/a/26618230/2312300
I have following code in my tests module
def teardown_module():
clean_database()
def test1(): pass
def test2(): assert 0
and I want teardown_module() (some cleanup code) to be called only if some test failed. Otherwise (if all passed) this code shouldn't have to be called.
Can I do such a trick with PyTest?
You can. But it is a little bit of a hack.
As written here: http://pytest.org/latest/example/simple.html#making-test-result-information-available-in-fixtures
you do the following, to set up an attribute for saving the status of each phase of the testcall:
# content of conftest.py
import pytest
#pytest.mark.tryfirst
def pytest_runtest_makereport(item, call, __multicall__):
rep = __multicall__.execute()
setattr(item, "rep_" + rep.when, rep)
return rep
and in the fixture you just examine the condition on those attributes like this:
import pytest
#pytest.yield_fixture(scope="module", autouse=True)
def myfixture(request):
print "SETUP"
yield
# probably should not use "_collected" to iterate over test functions
if any(call.rep_call.outcome != "passed" for call in request.node._collected):
print "TEARDOWN"
This way if any of the tests associated with that module fixture is not "passed" (so "failed" or "skipped") then the condition holds.
The answer posted here and link to documentation was helpful but not sufficient for my needs. I needed a module teardown function to execute for each module independently if any test in that module (.py) file failed.
A complete sample project is available on GitHub
To start with, we need a hook to attach the test function result to
the test node. This is taken directly from the pytest docs:
# in conftest.py
#pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
# set a report attribute for each phase of a call, which can
# be "setup", "call", "teardown"
var_name = "rep_" + rep.when
setattr(item, var_name, rep)
After that, we need another hook for the test case to find the module and
store itself there, so the module can easily find its test cases. Perhaps
there's a better way, but I was unable to find one.
# also in conftest.py
#pytest.fixture(scope="function", autouse=True)
def _testcase_exit(request):
yield
parent = request.node.parent
while not isinstance(parent, pytest.Module):
parent = parent.parent
try:
parent.test_nodes.append(request.node)
except AttributeError:
parent.test_nodes = [request.node]
Once we do that, it's nice to have a decorator function to have the module on
completion look through its test nodes, find if there are any failures, and
then if there were call the function associated with the decorator:
# also also in conftest.py
def module_error_teardown(f):
#wraps(f)
#pytest.fixture(scope="module", autouse=True)
def wrapped(request, *args, **kwargs):
yield
try:
test_nodes = request.node.test_nodes
except AttributeError:
test_nodes = []
something_failed = False
for x in test_nodes:
try:
something_failed |= x.rep_setup.failed
something_failed |= x.rep_call.failed
something_failed |= x.rep_teardown.failed
except AttributeError:
pass
if something_failed:
f(*args, **kwargs)
return wrapped
Now we have all the necessary framework to work with. Now, a test file with a failing test case is easy to write:
from conftest import module_error_teardown
def test_something_that_fails():
assert False, "Yes, it failed."
def test_something_else_that_fails():
assert False, "It failed again."
#module_error_teardown
def _this_gets_called_at_the_end_if_any_test_in_this_file_fails():
print('')
print("Here's where we would do module-level cleanup!")