PySlackers - A community for Python enthusiasts

Tidbit: Testing a Python library with PyTest

1 year, 4 months ago  by: matt

Testing Pytest Tidbit

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.

The Project Structure

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.

Testing

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:

  1. As a not-installed library (against the source-code in the project directory)
  2. As an installed library (against the installed package in the interpreter's site-packages)

Not-installed Library

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:

  1. No need for an extra install step before testing
  2. Easy way to test incremental changes
  3. Avoid cluttering the current virtualenvironment's packages (it simplifies pip freeze)

Installed Library

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:

  1. Ensure the setup.py runs as expected
  2. Provide a way to "integrity check" an installed package (beyond checksums)

Knitty Gritty

For 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.