I am writing tests using Pytest and Python 3.6. My tests use an ENV var called online, which I use to determine whether the tests will hit the real web service or whether it will just be playback.
I want to patch sleep when the tests are not online for all the tests. When the tests are in online mode, dont patch. Is there a way to do this on pytest? My initial thought was to use a fixture that uses the env var to monkeypatch or not.
I am not sure how to go about with this?
The easiest way to do this is to use an autouse fixture. The standard way is to use mock.patch to mock the function:
from unittest import mock
import pytest
#pytest.fixture(autouse=True)
def patch_sleep():
if "ONLINE" in os.environ and os.environ["ONLINE"] == "1":
with mock.patch("time.sleep"):
yield
else:
yield
In this case the function will be patched in each test, and the patch reverted after each test. If you use autouse=True, the fixture will be invoked automatically in all tests, otherwise you have to reference it in the tests.
If you know that your environment cannot change during the test, you can use a session-scoped fixture that is invoked only once in the test session:
#pytest.fixture(autouse=True, scope="session")
def patch_sleep():
...
In this case, time.sleep will be patched before all tests run, and the patch reverted after all tests have finished.
If you don't want to use unittest.patch, you can use the pytest-mock plugin, which provides the mocker fixture:
#pytest.fixture(autouse=True)
def patch_sleep(mocker):
if "ONLINE" in os.environ and os.environ["ONLINE"] == "1":
mocker.patch("time.sleep")
yield
else:
yield
You can also use monkeypatch, though that is not the usual way for patching a function:
#pytest.fixture(autouse=True)
def patch_sleep(monkeypatch):
def dont_sleep(sec):
pass
if "ONLINE" in os.environ and os.environ["ONLINE"] == "1":
monkeypatch.setattr(time, "sleep", dont_sleep)
yield
else:
yield
Related
I would like to know if there is a way to access a pytest fixture from functions located in other modules. I wonder if I could use custom pytest hooks for this.
# test/test_file.py
pytest.fixture
def my_fixture()
return my_object
def test_function():
print('Starting test)
my_function('arg1')
# lib/functions.py
def my_function(arg1):
# I would like have access to my_fixture
So since it's possible to access fixtures in pytest hooks during runtime session, I wonder if there is a way to access them from outside of the test functions also.
So far this solution works for me:
#pytest.mark.hookwrapper
def pytest_runtest_makereport(self, item, call):
# Stops at first non-None test result
outcome = yield
rep = outcome.get_result()
if rep.when == 'call' and hasattr(item, 'callspec'):
test_case = item.callspec.params.get('test_case')
Here I can access my_fixture thingy, but only in a pre-defined hooks, pytest standard flow. I wonder if I could create my own hooks, and then make my functions "aware" of these and get the access to anything defined inside.
How to define and use custom hooks?
We've recently switched from unittest to pytest. I've encountered a strange problem when using mocker.patch as a context manager. Consider the following example.
module_a.py
class MyClass:
def value(self):
return 10
module_b.py
import module_a
class AnotherClass:
def get_value(self):
return module_a.MyClass().value()
test_module_b.py
from module_b import AnotherClass
def test_main_2(mocker):
with mocker.patch('module_a.MyClass.value', return_value=20):
value = AnotherClass().get_value()
assert value == 20
value = AnotherClass().get_value()
assert value == 10
I would expect that once the context manager exits, MyClass's value method method would be restored (return value of 10), however the test fails on the second assert statement with an assertion error 20 != 10 If I use the exact same test, but replace mocker.patch with unittest.mock.patch, it passes. I thought that pytest-mock shared the same API as unittest.mock, so I'm confused as to why there is a difference.
With pytest-mock, teardown is done when exiting the fixture context. The mocker.patch object is not just a simple alias for mock.patch.
You should not need context managers within the test functions when writing pytest-style tests, and in fact the purpose of the pytest-mock plugin is to make the use of context managers and function decorators for mocking unnecessary.
If for some reason you do need a teardown step from within the test itself, then you want the plain old mock API which also works fine within pytest.
from unittest.mock import patch
def test_main_2():
with patch('module_a.MyClass.value', return_value=20):
value = AnotherClass().get_value()
assert value == 20
value = AnotherClass().get_value()
assert value == 10
Be aware that this nested structure is really what pytest intends to avoid, to make your tests more readable, so you're somewhat missing the point if you don't do setup and teardown entirely with fixtures.
I'm relatively new to pytest-style unit testing, and I'm trying to learn more about pytest fixtures. I'm not passing a scope argument to the fixture, so I know that the scope is "function". Are there any functional differences in these 3 styles of simple fixtures? Why would one approach be favored over the others?
#pytest.fixture()
#patch('a.b.c.structlog.get_logger')
def fixture_classQ(mock_log):
handler = MagicMock(spec=WidgetHandler)
return ClassQ(handler)
#pytest.fixture()
def fixture_classQ():
with patch('a.b.c.structlog.get_logger'):
handler = MagicMock(spec=WidgetHandler)
return ClassQ(handler)
#pytest.yield_fixture()
def fixture_classQ():
with patch('a.b.c.structlog.get_logger'):
handler = MagicMock(spec=WidgetHandler)
yield ClassQ(handler)
Simple example usage of the fixture:
def test_classQ_str(fixture_classQ):
assert str(fixture_classQ) == "This is ClassQ"
Thanks.
fixture 1
Starting with the first one, this creates a plain-data fixture. The mock is (imo misleadingly) only alive for the duration of the fixture function because it uses return.
In order ~roughly what happens for that:
pytest notices your fixture is used for the test function
it calls the fixture function
the mock decorator starts the patch
the mock decorator calls your actual function (which returns a value)
the mock decorator undoes the patch
pytest notices it wasn't a generator and so that's the value of your fixture
fixture 2
the second is identical in behaviour to the first, except it uses the context manager form of mock instead of the decorator. personally I don't like the decorator form but that's just me :D
fixture 3
(first before I continue, pytest.yield_fixture is a deprecated alias for pytest.fixture -- you can just use #pytest.fixture)
The third does something different! The patch is alive for the duration of the test because it has "yielded" during the fixture. This is a kind of way to create a setup + teardown fixture all in one. Here's roughly the execution here
pytest notices your fixture is used for the test function
pytest calls the fixture function
since it is a generator, it returns immediately without executing code
pytest notices it is a generator, calls next(...) on it
this causes the code to execute until the yield and then "pausing". you can think of it kind of as a co-routine
the __enter__ of the mock is called making the patch active
the value that is yielded is used as the value of the fixture
pytest then executes your test function
pytest then calls next(...) again on the generator to exhaust the fixture
this __exit__s the with statement, undoing the patch
which to choose?
the best answer is it depends. Since 1 and 2 are functionally equivalent it's up to personal preference. Pick 3. if you need the patch to be active during the entire duration of your test. And don't use pytest.yield_fixture, just use pytest.fixture.
I use the following to mock constant values for a test with py.test:
#patch('ConstantsModule.ConstantsClass.DELAY_TIME', 10)
def test_PowerUp():
...
thing = Thing.Thing()
assert thing.a == 1
This mocks DELAY_TIME as used in both the test, and in Thing, which is what I expected.
I wanted to do this for all the tests in this file, so I tried
#patch('ConstantsModule.ConstantsClass.DELAY_TIME', 10)
#pytest.fixture(autouse=True)
def NoDelay():
pass
But that doesn't seem to have the same effect.
Here is a similar question: pytest-mock mocker in pytest fixture, but the mock seems done in a non-decorator way there.
I'd say patching via decorator is not the optimal approach here. I'd use the context manager:
import pytest
from unittest.mock import patch
#pytest.fixture(autouse=True)
def no_delay():
with patch('ConstantsModule.ConstantsClass.DELAY_TIME', 10):
yield
This way, patching is cleanly reverted on test teardown.
pytest provides builtin patching support via the monkeypatch fixture. So to patch the constant for all tests in the file you can create the following autouse fixture:
#pytest.fixture(autouse=True)
def no_delay(monkeypatch):
monkeypatch.setattr(ConstantsModule.ConstantsClass, 'DELAY_TIME', 10)
If you don't want to have the ConstantsModule imported in your tests you can use a string, see the full API reference.
Is it possible to prevent the execution of "function scoped" fixtures with autouse=True on specific marks only?
I have the following fixture set to autouse so that all outgoing requests are automatically mocked out:
#pytest.fixture(autouse=True)
def no_requests(monkeypatch):
monkeypatch.setattr("requests.sessions.Session.request", MagicMock())
But I have a mark called endtoend that I use to define a series of tests that are allowed to make external requests for more robust end to end testing. I would like to inject no_requests in all tests (the vast majority), but not in tests like the following:
#pytest.mark.endtoend
def test_api_returns_ok():
assert make_request().status_code == 200
Is this possible?
You can also use the request object in your fixture to check the markers used on the test, and don't do anything if a specific marker is set:
import pytest
#pytest.fixture(autouse=True)
def autofixt(request):
if 'noautofixt' in request.keywords:
return
print("patching stuff")
def test1():
pass
#pytest.mark.noautofixt
def test2():
pass
Output with -vs:
x.py::test1 patching stuff
PASSED
x.py::test2 PASSED
In case you have your endtoend tests in specific modules or classes you could also just override the no_requests fixture locally, for example assuming you group all your integration tests in a file called end_to_end.py:
# test_end_to_end.py
#pytest.fixture(autouse=True)
def no_requests():
return
def test_api_returns_ok():
# Should make a real request.
assert make_request().status_code == 200
I wasn't able to find a way to disable fixtures with autouse=True, but I did find a way to revert the changes made in my no_requests fixture. monkeypatch has a method undo that reverts all patches made on the stack, so I was able to call it in my endtoend tests like so:
#pytest.mark.endtoend
def test_api_returns_ok(monkeypatch):
monkeypatch.undo()
assert make_request().status_code == 200
It would be difficult and probably not possible to cancel or change the autouse
You can't canel an autouse, being as it's autouse. Maybe you could do something to change the autouse fixture based on a mark's condition. But this would be hackish and difficult.
possibly with:
import pytest
from _pytest.mark import MarkInfo
I couldn't find a way to do this, but maybe the #pytest.fixture(autouse=True) could get the MarkInfo and if it came back 'endtoend' the fixture wouldn't set the attribute. But you would also have to set a condition in the fixture parameters.
i.e.: #pytest.fixture(True=MarkInfo, autouse=True). Something like that. But I couldn't find a way.
It's recommended that you organize tests to prevent this
You could just separate the no_requests from the endtoend tests by either:
limit the scope of your autouse fixture
put the no_requests into a class
Not make it an auto use, just pass it into the params of each def you need it
Like so:
class NoRequests:
#pytest.fixture(scope='module', autouse=True)
def no_requests(monkeypatch):
monkeypatch.setattr("requests.sessions.Session.request", MagicMock())
def test_no_request1(self):
# do stuff here
# and so on
This is good practice. Maybe a different organization could help
But in your case, it's probably easiest to monkeypatch.undo()