This note covers the conventions for writing tests in proto-tools, including structure, assertions, markers, and naming.
All tests use flat functions (no test classes). Follow these patterns when writing new tests.
- One-liner module docstring:
"""Tests for {tool/entity name}.""" - No
from __future__ import annotationsanywhere in the codebase - Flat functions only: No
class Test*. Use descriptive function names (e.g.,test_blast_search_exact_match) - Section separators: Use
# ── Section name ──...for groups. Use# ---------------------------------------------------------------------------+# Integration testsfor the integration boundary - File ordering: Unit tests first, then the integration boundary separator, then integration/GPU tests
- Module-level fixtures: Use
@pytest.fixtureat module level, not inside classes. Usescope="module"for expensive setup - Module-level constants: Deduplicate repeated values as
_PREFIXEDconstants (e.g.,_SETUP_SH,_CRISPR_SEQUENCE) - Test directory naming:
tests/{category}_tests/matchingtools/{category}/
- Specific exception matching: Always use
pytest.raises(ExceptionType, match="..."). Never use barepytest.raises(Exception). For Pydanticge=Nconstraints, match"greater than or equal to N" - No trivial tests: Don't test that Pydantic stores default values. Test computed properties, validators, normalization, and error cases
tmp_pathovertempfile: Use pytest's built-intmp_pathfixture
Canonical reference is pyproject.toml [tool.pytest.ini_options].markers. Tool-author cheat sheet:
@pytest.mark.integration: Tests callingToolInstance.dispatch()for CPU tools. Skipped by default. Run with--integration@pytest.mark.uses_gpu: Tests callingToolInstance.dispatch()for GPU tools. Auto-skipped when no GPU. Implies environment requirement. Do not also add@pytest.mark.integration. Optional arg@pytest.mark.uses_gpu(n)requiresnvisible GPUs@pytest.mark.uses_cpu: CPU-only test. Optional arg@pytest.mark.uses_cpu(n)requiresnCPUs (matches whatToolPool._detect_cpuswould see)@pytest.mark.slow: Tests that may take several minutes. Skipped by default. Run with--slowor--all@pytest.mark.extensive: Combinatorial tests (e.g. every tool × device transition). Skipped unless--ext(or--extensive) is passed@pytest.mark.benchmark('<tool-key>'): E2E benchmark for one tool. Required arg. Skipped by default. Opt in via--benchmark(or--benchmark-report/--benchmark-tool/--benchmark-toolkit). Not enabled by--allor--slow@pytest.mark.skip_ci: Only for tests requiring optional/external dependencies not inpyproject.toml. Do not add for core deps@pytest.mark.include_in_env_report: Applied automatically bytest_env_report.pyparametrization. Do not add manually@pytest.mark.test_on_platforms('x86_64', ...): Restrict to specific architectures
- Validation tests:
test_{model}_rejects_{what} - Property tests:
test_{model}_{property} - Export tests:
test_export_{format} - Integration tests:
test_{tool}_{scenario}
pytest-randomly shuffles test order each run to catch hidden inter-test dependencies. The seed is printed in the test header. Reproduce a specific order with --randomly-seed=N.