I have been familiarizing with pytest lately and on how you can use conftest.py
to define fixtures that are automatically discovered and imported within my tests. It is pretty clear to me how conftest.py
works and how it can be used, but I'm not sure about why this is considered a best practice in some basic scenarios.
Let's say my tests are structured in this way:
tests/
--test_a.py
--test_b.py
The best practice, as suggested by the documentation and various articles about pytest around the web, would be to define a conftest.py
file with some fixtures to be used in both test_a.py
and test_b.py
. In order to better organize my fixtures, I might have the need of splitting them into separate files in a semantically meaningful way, ex. db_session_fixtures.py
, dataframe_fixtures.py
, and then import them as plugins in conftest.py
.
tests/
--test_a.py
--test_b.py
--conftest.py
--db_session_fixtures.py
--dataframe_fixtures.py
In conftest.py
I would have:
import pytest
pytest_plugins = ["db_session_fixtures", "dataframe_fixtures"]
and I would be able to use db_session_fixtures
and dataframe_fixtures
seamlessly in my test cases without any additional code.
While this is handy, I feel it might hurt readability. For example, if I would not use conftest.py
as described above, I might write in test_a.py
from .dataframe_fixtures import my_dataframe_fixture
def test_case_a(my_dataframe_fixture):
#some tests
and use the fixtures as usual.
The downside is that it requires me to import the fixture, but the explicit import improves the readability of my test case, letting me know in a glance where the fixture come from, just as any other python module.
Are there downsides I am overlooking on about this solution or other advantages that conftest.py
brings to the table, making it the best practice when setting up pytest test suites?
There's not a huge amount of difference, it's mainly just down to preference. I mainly use conftest.py
to pull in fixures that are required, but not directly used by your test. So you may have a fixture that does something useful with a database, but needs a database connection to do so. So you make the db_connection
fixture available in conftest.py
, and then your test only has to do something like:
conftest.py
from tests.database_fixtures import db_connection
__all__ = ['db_connection']
tests/database_fixtures.py
import pytest
@pytest.fixture
def db_connection():
...
@pytest.fixture
def new_user(db_connection):
...
test/test_user.py
from tests.database_fixtures import new_user
def test_user(new_user):
assert new_user.id > 0 # or whatever the test needs to do
If you didn't make db_connection
available in conftest.py
or directly import it then pytest would fail to find the db_connection
fixture when trying to use the new_user
fixture. If you directly import db_connection
into your test file, then linters will complain that it is an unused import. Worse, some may remove it, and cause your tests to fail. So making the db_connection
available in conftest.py
, to me, is the simplest solution.
The one significant difference is that it is easier to override fixtures using conftest.py
. Say you have a directory layout of:
./
├─ conftest.py
└─ tests/
├─ test_foo.py
└─ bar/
├─ conftest.py
└─ test_foobar.py
In conftest.py
you could have:
import pytest
@pytest.fixture
def some_value():
return 'foo'
And then in tests/bar/conftest.py
you could have:
import pytest
@pytest.fixture
def some_value(some_value):
return some_value + 'bar'
Having multiple conftests allows you to override a fixture whilst still maintaining access to the original fixture. So following tests would all work.
tests/test_foo.py
def test_foo(some_value):
assert some_value == 'foo'
tests/bar/test_foobar.py
def test_foobar(some_value):
assert some_value == 'foobar'
You can still do this without conftest.py
, but it's a bit more complicated. You'd need to do something like:
import pytest
# in this scenario we would have something like:
# mv contest.py tests/custom_fixtures.py
from tests.custom_fixtures import some_value as original_some_value
@pytest.fixture
def some_value(original_some_value):
return original_some_value + 'bar'
def test_foobar(some_value):
assert some_value == 'foobar'