Testing in Python
Overall Structure
I structure small projects with an entrypoint named after the project itself,
or simply app.py
, a directory called lib/
that contains modules used in
the entrypoint, and a director called test/
containing tests. Inside the
project root, and both lib/
and test/
directories, are empty __init__.py
files so that the Python interpreter can load modules from the lib/
directory
in the test/
directory.
Below is the typical shape of a small application.
project
├── README.md
├── .gitignore
├── __init__.py
├── lib
│ ├── __init__.py
│ ├── adapters.py
│ ├── core.py
│ └── interfaces.py
├── app.config.yaml
├── app.py
├── pyproject.toml
├── requirements.txt
├── test
│ ├── __init__.py
│ ├── data
│ ├── conftest.py
│ └── test.py
└── venv
└── ...
Testing Modules
Inside test/test.py
I’ll import modules from the sibling directory lib/
as
from ..lib import (
adapters,
core,
interfaces,
)
Organizing Fixtures
I keep all test fixtures in test/conftest.py
which magically does not need
to be imported into each test script.
When a fixture needs to be cleaned up after a test, I yield the fixture instead of returning it.
import os
import pathlib
import pytest
from ..lib import (
adapters,
)
@pytest.fixture
def formation_adapter():
'''Yield a formation adapter and then destroy it.'''
path = pathlib.Path('test/data/test.duckdb')
path = str(path.absolute())
fmn_adapter = adapters.FormationDuckDBAdapter(path)
fmn_adapter.create_formation_table()
yield fmn_adapter
os.remove(path)
Running Tests with Coverage
I run scripts from the root of the project using coverage, pytest, and toml.
Coverage configuration goes into pyproject.toml
as,
[tool.coverage.run]
omit = ["test/*", "*/__init__.py"]
To run the tests I execute,
coverage run -m pytest test -v
coverate report -m