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:
site-packages
)First off, leveraging this structure you can test the "not-installed library" (your project's root directory) directly with:
1 | (.venv) $ python -m pytest |
The benefits are probably pretty clear if you have developed any number of libraries:
pip freeze
)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.py
runs as expectedFor inquiring minds, the details revolve around how Python resolves dependencies via the PYTHONPATH
. 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.