Testing Framework¶
This document covers testing for the _extensions/ and _tools/ Python code.
Running Tests¶
All tests use pytest and are run via mise:
# Run all tests
mise run test
# Run tests in watch mode (TDD)
mise run test:watch
# Run with coverage report
.venv/bin/pytest --cov=_extensions --cov=_tools --cov-report=term-missing
# Run specific module tests
.venv/bin/pytest _extensions/category_nav/tests/ -v
Test Structure¶
Tests are colocated with their modules:
_extensions/
├── category_nav/
│ ├── __init__.py
│ ├── directive.py
│ └── tests/
│ ├── __init__.py
│ └── test_category_nav.py
├── publish_filter/
│ └── tests/
│ └── test_publish_filter.py
└── ...
_tools/
├── antibot_filter.py
├── test_antibot_filter.py # Colocated at same level
└── frontmatter_normalizer/
└── tests/
├── conftest.py # Shared fixtures
├── test_cli.py
└── ...
Configuration¶
Test configuration lives in pyproject.toml:
[tool.pytest.ini_options]
pythonpath = ["_extensions", "_tools"]
testpaths = ["_extensions", "_tools"]
addopts = "-v"
Coverage Targets¶
All modules should maintain 90%+ coverage. Current status:
Module |
Coverage |
|---|---|
|
100% |
|
100% |
|
95% |
|
100% |
|
93% |
|
92% |
|
92% |
Testing Patterns¶
Mocking Sphinx App Objects¶
Sphinx extensions receive an app object. Mock it for unit tests:
from unittest.mock import Mock
def test_builder_inited(tmp_path):
app = Mock()
app.srcdir = str(tmp_path)
app.config.exclude_patterns = []
app.env.metadata = {}
# Call your function
builder_inited(app)
# Assert on mock
assert 'draft.md' in app.config.exclude_patterns
Testing Sphinx Directives¶
Sphinx directives have env and config as properties. Use patch.object:
from unittest.mock import Mock, patch
def test_directive_run(tmp_path):
from category_nav.directive import CategoryNavDirective
mock_env = Mock()
mock_env.srcdir = str(tmp_path)
mock_env.docname = 'index'
mock_config = Mock()
mock_config.category_nav_default = 'Miscellaneous'
with patch.object(CategoryNavDirective, 'env', mock_env), \
patch.object(CategoryNavDirective, 'config', mock_config):
directive = object.__new__(CategoryNavDirective)
result = directive.run()
Testing Event Handlers¶
Verify that setup() connects the right events:
def test_connects_events():
from my_extension import setup
app = Mock()
setup(app)
event_names = [call[0][0] for call in app.connect.call_args_list]
assert 'builder-inited' in event_names
assert 'source-read' in event_names
Testing CLI Commands¶
Use Click’s CliRunner for CLI tests:
from click.testing import CliRunner
def test_normalize_command(tmp_path):
from frontmatter_normalizer.cli import main
(tmp_path / 'test.md').write_text('# Title')
runner = CliRunner()
result = runner.invoke(main, ['normalize', str(tmp_path)])
assert result.exit_code == 0
assert 'files' in result.output.lower()
Testing with stdin/stdout¶
For filters that use stdin/stdout:
from io import StringIO
def test_main_clean_mode(monkeypatch):
import sys
from antibot_filter import main, ZWS
monkeypatch.setattr(sys, 'argv', ['script.py', '--clean'])
monkeypatch.setattr(sys, 'stdin', StringIO("Hello world"))
stdout = StringIO()
monkeypatch.setattr(sys, 'stdout', stdout)
main()
assert ZWS in stdout.getvalue()
Writing New Tests¶
Follow the TDD approach:
1. Red: Write a failing test that defines expected behavior 2. Green: Write minimal code to make the test pass 3. Refactor: Clean up while keeping tests green
Example test structure:
class TestMyFeature:
"""Tests for feature X."""
def test_basic_case(self):
"""Should handle the common case."""
# Arrange
input_data = "..."
# Act
result = my_function(input_data)
# Assert
assert result == expected
def test_edge_case(self):
"""Should handle empty input."""
result = my_function("")
assert result == ""
Troubleshooting¶
Tests can‘t import modules: Check pythonpath in pyproject.toml
**Coverage not measured: Ensure --cov flag matches module path exactly
Sphinx imports fail: Install dev dependencies: .venv/bin/pip install -e .