Injecting modules into tests with Pytest
There is a known slightly boring problem with tests. You have conftest.py
, which is magical and is always available in tests (but only via hooks and fixtures, and double-magical ‘request’ metafunc). If you want to have it outside fixtures, things become complicated.
You can have an additional library, but if your tests are in complicated structure (like this):
tests/
|- infra/
|- monitoring/
|- logging/
|- security/
|- applications/
|- foo/
|- smoke
|- long
then you have a problem with import. You can get away with it (relative pathes, plus __init__.py
everywhere), but it’s not elegant, and it slows down restructuring tests. You no longer can just ‘mv’ them, you need to update imports.
The thing I wanted, in any test file, at any nested directory, ability to write:
from mytestlib import singleton
And I want this singleton been created by conftest (because everything interesting is happening there).
I had had few tricks to do it before, but they all was… meh.
Now a cool one, credited to… OpenAI. (And my persistence on correcting hallucinations; turned out, one of hallucinations was real).
In the conftest.py
class MyTestLib:
@property
def singleton(self):
call_to_conftest_internals()
sys.modules["mytestlib"] = MyTestLib()
That’s all. Now, every test module (a file *test*.py
discovered by pytest) can do this:
from mytestlib import singleton
This magic with class pretending to be a module… There is beauty in Python.
Why? Why do you need it when you have fixtures?
Why do I want to use it instead of a normal fixture?
A fixture will be used like this:
def test_my(singleton):
...
And declared like this (in conftest.py
):
@pytest.fixture(scope="module")
def singleton():
gloabl SINGLETON # created by other conftest.py parts
return SINGLETON
Generally, this is the Pytest way to do things.
It has one problem: you can’t use it as a parameter to the fixture.
Let’s say I want this:
@pytest.mark.parametrize("foo", singleton)
def test_is_good(foo):
assert foo.is_good()
You can’t. Fixtures are callable after test generations, and this is ground truth for Pytest. Nope, None. Not, No, Don’t.
With my precious you can (I can!):
from mytestlib import singleton
@pytest.mark.parametrize("foo", singleton)
def test_is_good(foo):
assert foo.is_good()
All you have to do is to fill it with content inside of conftest.py
, which is usually done in pytest_configure
function.
Conclusion
Do I need a conclusion? This trick is awesome.