Tests are a way for us to ensure that something works as intended.
Types of Tests
- Unit tests: tests on individual components that each have a single responsibility (ex. a function).
- Integration tests: tests on the combined functionality of individual components (ex. data processing).
- System tests: tests on the design of a system for expected outputs given inputs (ex. training, inference, etc.).
- Acceptance tests: tests to verify that requirements have been met, usually referred to as User Acceptance Testing (UAT).
- Regression tests: tests based on errors we’ve seen before to ensure new changes don’t reintroduce them.
Framework For Tests
- Arrange: set up the different inputs to test on.
- Act: apply the inputs on the component we want to test.
- Assert: confirm that we received the expected output.
What to test?
- Inputs: data types, format, length, edge cases (min/max, small/large, etc.)
- Outputs: data types, formats, exceptions, intermediary and final outputs
Best Practices
- Atomic: when creating functions and classes, we need to ensure that they have a single responsibility so that we can easily test them. If not, we’ll need to split them into more granular components.
- Compose: when we create new components, we want to compose tests to validate their functionality. It’s a great way to ensure reliability and catch errors early on.
- Reuse: we should maintain central repositories where core functionality is tested at the source and reused across many projects. This significantly reduces testing efforts for each new project’s code base.
- Regression: we want to account for new errors we come across with a regression test so we can ensure we don’t reintroduce the same errors in the future.
- Coverage: we want to ensure 100% coverage for our codebase. This doesn’t mean writing a test for every single line of code but rather accounting for every single line.
- Automate: in the event we forget to run our tests before committing to a repository, we want to auto run tests when we make changes to our codebase.
Test Driven Development (TDD)
- Red
- Green
- Refactor
Implementation
Project Structure
tests/
├── code/
│ ├── ...
│ └── ...
├── data/
│ ├── ...
│ └── ...
└── models/
│ ├── ...
│ └── ...
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
Coverage
We can use the Coverage library to track and visualize how much of our codebase our tests account for.
Exclusions: Excusing lines by adding this comment # pragma: no cover, <MESSAGE>
Excluding files by specifying them in our pyproject.toml
 configuration.
# pyproject.toml
[tool.coverage.run]
omit=["project/evaluate.py", "project/serve.py"]
Parametrize
Pytest has the @pytest.mark.parametrize
decorator which allows us to represent our inputs and outputs as parameters.
Fixtures
MISSING
Markers
We can execute our tests at various levels of granularity (all tests, script, function, etc.) but we can create custom granularity by using markers.
The proper way to use markers is to explicitly list the ones we’ve created in ourÂ
pyproject.toml
 file.
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
addopts = "--strict-markers --disable-pytest-warnings"
markers = [
"training: tests that involve training",
]