doc: update and improve import mode docs
This commit is contained in:
parent
c85fce39b6
commit
5746b8e696
|
@ -60,8 +60,10 @@ Within Python modules, ``pytest`` also discovers tests using the standard
|
||||||
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
||||||
|
|
||||||
|
|
||||||
Choosing a test layout / import rules
|
.. _`test layout`:
|
||||||
-------------------------------------
|
|
||||||
|
Choosing a test layout
|
||||||
|
----------------------
|
||||||
|
|
||||||
``pytest`` supports two common test layouts:
|
``pytest`` supports two common test layouts:
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,23 @@ changing :data:`sys.path`. Some aspects of the
|
||||||
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
||||||
these values:
|
these values:
|
||||||
|
|
||||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
.. _`import-mode-prepend`:
|
||||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
|
||||||
|
|
||||||
This requires test module names to be unique when the test directory tree is not arranged in
|
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||||
packages, because the modules will put in :py:data:`sys.modules` after importing.
|
of :py:data:`sys.path` if not already there, and then imported with
|
||||||
|
the :func:`importlib.import_module <importlib.import_module>` function.
|
||||||
|
|
||||||
|
It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories
|
||||||
|
containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full
|
||||||
|
name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package).
|
||||||
|
|
||||||
|
If the test directory tree is not arranged as packages, then each test file needs to have a unique name
|
||||||
|
compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name.
|
||||||
|
|
||||||
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
||||||
|
|
||||||
|
.. _`import-mode-append`:
|
||||||
|
|
||||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||||
|
|
||||||
|
@ -38,18 +47,55 @@ these values:
|
||||||
the tests will run against the installed version
|
the tests will run against the installed version
|
||||||
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
||||||
with ``prepend`` they would pick up the local version. This kind of confusion is why
|
with ``prepend`` they would pick up the local version. This kind of confusion is why
|
||||||
we advocate for using :ref:`src <src-layout>` layouts.
|
we advocate for using :ref:`src-layouts <src-layout>`.
|
||||||
|
|
||||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||||
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||||
|
|
||||||
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
.. _`import-mode-importlib`:
|
||||||
|
|
||||||
For this reason this doesn't require test module names to be unique.
|
* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`.
|
||||||
|
|
||||||
One drawback however is that test modules are non-importable by each other. Also, utility
|
Advantages of this mode:
|
||||||
modules in the tests directories are not automatically importable because the tests directory is no longer
|
|
||||||
added to :py:data:`sys.path`.
|
* pytest will not change :py:data:`sys.path` at all.
|
||||||
|
* Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``.
|
||||||
|
|
||||||
|
Disadvantages:
|
||||||
|
|
||||||
|
* Test modules can't import each other.
|
||||||
|
* Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes)
|
||||||
|
are not importable. The recommendation in this case it to place testing utility modules together with the application/library
|
||||||
|
code, for example ``app.testing.helpers``.
|
||||||
|
|
||||||
|
Important: by "test utility modules" we mean functions/classes which are imported by
|
||||||
|
other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along
|
||||||
|
with the test modules, and are discovered automatically by pytest.
|
||||||
|
|
||||||
|
It works like this:
|
||||||
|
|
||||||
|
1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name
|
||||||
|
like ``tests.core.test_models`` and tries to import it.
|
||||||
|
|
||||||
|
For non-test modules this will work if they are accessible via :py:data:`sys.path`, so
|
||||||
|
for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``.
|
||||||
|
This is happens when plugins import non-test modules (for example doctesting).
|
||||||
|
|
||||||
|
If this step succeeds, the module is returned.
|
||||||
|
|
||||||
|
For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail.
|
||||||
|
|
||||||
|
2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without
|
||||||
|
changing :py:data:`sys.path`.
|
||||||
|
|
||||||
|
Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based
|
||||||
|
on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`.
|
||||||
|
|
||||||
|
For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
||||||
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
||||||
|
@ -58,12 +104,14 @@ these values:
|
||||||
|
|
||||||
The :confval:`pythonpath` configuration variable.
|
The :confval:`pythonpath` configuration variable.
|
||||||
|
|
||||||
|
:ref:`test layout`.
|
||||||
|
|
||||||
|
|
||||||
``prepend`` and ``append`` import modes scenarios
|
``prepend`` and ``append`` import modes scenarios
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
|
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
|
||||||
change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users
|
change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users
|
||||||
might encounter because of that.
|
might encounter because of that.
|
||||||
|
|
||||||
Test modules / ``conftest.py`` files inside packages
|
Test modules / ``conftest.py`` files inside packages
|
||||||
|
@ -92,7 +140,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa
|
||||||
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
|
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
|
||||||
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
|
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
|
||||||
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
|
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
|
||||||
``sys.path`` (if not there already) in order to load
|
:py:data:`sys.path` (if not there already) in order to load
|
||||||
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
|
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
|
||||||
|
|
||||||
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
|
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
|
||||||
|
@ -122,8 +170,8 @@ When executing:
|
||||||
|
|
||||||
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
|
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
|
||||||
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
|
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
|
||||||
``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
|
:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
|
||||||
with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``.
|
with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``.
|
||||||
|
|
||||||
For this reason this layout cannot have test modules with the same name, as they all will be
|
For this reason this layout cannot have test modules with the same name, as they all will be
|
||||||
imported in the global import namespace.
|
imported in the global import namespace.
|
||||||
|
@ -136,7 +184,7 @@ Invoking ``pytest`` versus ``python -m pytest``
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
||||||
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
|
equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which
|
||||||
is standard ``python`` behavior.
|
is standard ``python`` behavior.
|
||||||
|
|
||||||
See also :ref:`invoke-python`.
|
See also :ref:`invoke-python`.
|
||||||
|
|
Loading…
Reference in New Issue