pytest has been exploding onto the Python scene for the last several years as the go-to test runner for many projects. Not only is it compatible with the standard unittest setup, it extends on it with incredibly powerful features (which are the topic for a future post).
Today we are going to discuss a powerful way to leverage pytest for library authors, which allows testing the uninstalled code directly or the code as an installed dependency.
To leverage the flexibility you will be best served by using a project structure similar to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
$ tree . ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── libraryname │ ├── __init__.py │ ├── module1.py │ ├── module2.py ├── setup.cfg ├── setup.py ├── tests │ ├── test_module1.py │ └── test_module2.py └── tox.ini
Note that you are not using a
src directory to house the source code (given that the project root serves that need). The
tests directory is 100% flexible and fully up to how you'd like to structure tests (note that it does not have an
__init__.py there!). If the library is a single module, you can replace the
libraryname/ directory with your module,
libraryname.py. In either case, the base setup of the
libraryname is at the top-level of the project.
Now getting to why we are here, testing our code to ensure an end-user gets an, ideally, bug-free experience. Testing projects in this setup allow for testing two ways:
First off, leveraging this structure you can test the "not-installed library" (your project's root directory) directly with:
(.venv) $ python -m pytest
The benefits are probably pretty clear if you have developed any number of libraries:
Secondly you can use this structure to test the library after install, ensuring the
setup.py runs properly:
1 2 3
$ mktmpenv # if you use virtualenvwrapper, this makes a clean, temporary, virtualenv! (tmp-1234) $ pip install ~/PathTo/YourProject/libraryname (tmp-1234) $ pytest
You'd want to do this for a few reasons too:
setup.pyruns as expected
For inquiring minds, the details revolve around how Python resolves dependencies via the
PYTHONPATH acts just like the system
PATH in that it provides an ordered list of locations to resolve dependencies.
When you run
python -m <module> the current working directory is included at the beginning path, making it so the library code is resolved, by name, first. The imports in the test (and lib) code will get these imports.
Conversely when running
pytest (or other entrypoint) the shim that
pip put in place will keep the current path setup, and imports in the test/lib code will resolve the installed package.