pytest imports each test file as a module, and by default it names the
module after the file. So tests/unit/test_utils.py and
tests/integration/test_utils.py both become test_utils, and the second
import fails:
ERROR collecting tests/unit/test_utils.py
import file mismatch:
imported module 'test_utils' has this __file__ attribute:
.../tests/integration/test_utils.py
which is not the same as the test file we want to collect:
.../tests/unit/test_utils.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
Deleting __pycache__, as the hint suggests, does not help here. The
problem is the module name, not a stale file.
Give each test module its own name
The simplest fix is to rename one file, for example to
test_utils_integration.py.
Or add an empty __init__.py to tests/ and to each folder under it:
tests/__init__.py
tests/unit/__init__.py
tests/unit/test_utils.py
tests/integration/__init__.py
tests/integration/test_utils.py
A folder with an __init__.py is a package, so pytest names each module
after its path through the packages. The two modules become
tests.unit.test_utils and tests.integration.test_utils.
Or switch pytest to its importlib import mode. In that mode pytest names
each module after the file's path and leaves the import path alone. Two files
can then share a name with no __init__.py files. Set it in pyproject.toml:
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
testpaths = ["tests"]
pytest has no setting for the import mode, so pass the command-line flag
through addopts. testpaths tells pytest where to look for tests when you
run it without naming any. pytest 9 also reads a [tool.pytest] table, which
the next article covers. The ini_options table works on older versions
too.
The importlib mode has a cost. The test's folder is no longer on the import
path. So a test cannot import a helper module that sits next to it, as in
from helpers import make_user. Share setup through fixtures instead, or
move the helpers into a package your tests can import.
Share fixtures through conftest.py
A conftest.py file holds fixtures for the tests in its folder and in every
folder below it. pytest finds it on its own, and a test asks for one of its
fixtures by naming it as a parameter. A conftest.py in a subfolder can add
fixtures, or replace one that has the same name.
pytest loads conftest.py itself, so do not import it from a test. Put helper
functions that tests call directly in an ordinary module.
Make your code importable from the tests
Say your project keeps its code under src/:
pyproject.toml
src/shop/__init__.py
tests/test_total.py
Run pytest here, and the test's from shop import total fails with
ModuleNotFoundError: No module named 'shop'. pytest put tests/ on the
import path, not src/. Either of two fixes works.
The first tells pytest where your code is. pytest adds each pythonpath
folder, relative to the project root, to the import path for the run:
[tool.pytest.ini_options]
pythonpath = ["src"]
testpaths = ["tests"]
The second is an editable install. Run python -m pip install -e . in
your virtual environment. pip installs your project, but Python reads its
files from your src/ folder, so an edit shows up without reinstalling.
Check your understanding
Your tests live in tests/api/test_models.py and tests/db/test_models.py, and pytest reports "import file mismatch". Why does deleting __pycache__ not fix it?
Show answer
Hide answer
Both files import as a module named test_models, and Python can hold only
one module under a name. No stale file causes it, so clearing caches changes
nothing. Rename one file, add an __init__.py to tests/ and to
each folder under it, or switch to the importlib import mode.
Going further
pythonpath and an editable install both let tests read files straight
from src/. So a test can pass even when a file is missing from the package
you publish. A wheel is the built file that pip installs, and the one
you upload to PyPI. To test exactly what users get, build the wheel, install
it into a new virtual environment, and run the tests from outside the source
folder. print(shop.__file__) shows which copy a test imported. The
packaging track covers building wheels.
The rootdir line in pytest's header names the folder pytest treats as the
project root. Test IDs are paths relative to it, and the .pytest_cache
folder lives in it. pytest does not add it to the import path.
python -m pytest adds the current folder to the import path, and plain
pytest does not. In a flat project, with shop/ next to tests/, one
command can pass where the other fails. Pick one and use it in CI too.