test_executor_successful_sync_entire_repo: _get_git_commit is patched separately and never calls the real _run_command, so it does not consume a slot in mock_run.side_effect. The list reserved one anyway (labelled "git rev-parse HEAD"), which shifted "M README.md\n" one call late — git status --porcelain saw "" (no changes) instead, so execute() took the no-changes branch and the test asserted "Successfully synced" against "already up to date". Removed the phantom slot. Also gave the iterdir() mock items real string .name attributes: MagicMock(name="X") sets the mock's repr, not the .name attribute read by execute()'s `item.name != '.git'` check, so every item was being treated as non-.git regardless of the intended value. test_executor_sync_specific_paths, test_executor_handles_no_changes: mock_upstream_dir/mock_gitea_dir were built but never wired to mock_path.return_value, so `work_dir = Path(...)` and `upstream_dir = work_dir / "upstream"` resolved to a different, unconfigured auto-generated MagicMock. `source.name` on that mock is itself a MagicMock, not a string, so `', '.join(copied_paths)` raised TypeError. Wired mock_path.return_value to a work_dir mock whose __truediv__ yields the intended upstream/gitea mocks, matching the pattern the first test already used correctly. All three are test-side: doc_sync_executor.py is unchanged. Confirmed via git log -p that this file and its test have exactly one commit in this repo's history (the initial extraction), so there is no prior passing version to regress from — these tests appear to have never passed.
Scheduler Tests
Comprehensive test suite for The Scheduler using pytest.
Test Structure
tests/
├── conftest.py # Shared fixtures and configuration
├── test_api.py # API endpoint tests
├── test_example_executor.py # Example executor tests
├── test_doc_sync_executor.py # Documentation sync executor tests
└── README.md # This file
Running Tests
Run all tests
docker exec scheduler pytest
Run with coverage report
docker exec scheduler pytest --cov=src --cov-report=term-missing
Run specific test file
docker exec scheduler pytest tests/test_api.py
Run specific test class
docker exec scheduler pytest tests/test_api.py::TestHealthEndpoint
Run specific test
docker exec scheduler pytest tests/test_api.py::TestHealthEndpoint::test_health_endpoint_returns_healthy
Run tests by marker
# Run only unit tests
docker exec scheduler pytest -m unit
# Run only API tests
docker exec scheduler pytest -m api
# Run only executor tests
docker exec scheduler pytest -m executor
# Run only fast tests (exclude slow)
docker exec scheduler pytest -m "not slow"
Run with verbose output
docker exec scheduler pytest -v
Run with detailed failure output
docker exec scheduler pytest -vv --tb=long
Stop on first failure
docker exec scheduler pytest -x
Test Markers
Tests are organized with pytest markers:
@pytest.mark.unit- Fast unit tests with no external dependencies@pytest.mark.integration- Integration tests (may use database)@pytest.mark.api- API endpoint tests@pytest.mark.executor- Task executor tests@pytest.mark.slow- Tests that take a while to run
Coverage Reports
After running tests with coverage:
- Terminal: Shows missing lines in terminal output
- HTML: Open
htmlcov/index.htmlin browser for detailed report - JSON: Machine-readable coverage data in
coverage.json
View HTML coverage report:
# From host machine
open /home/jpmschweitzer/docker-data/scheduler/htmlcov/index.html
Writing New Tests
Test File Naming
- Test files must start with
test_ - Place in
tests/directory - Use descriptive names:
test_<module_name>.py
Test Function Naming
- Test functions must start with
test_ - Use descriptive names:
test_<what_is_being_tested>
Using Fixtures
Fixtures are defined in conftest.py:
def test_something(test_settings, auth_headers):
# Use fixtures as function parameters
assert test_settings.app_name == "Test Scheduler"
Adding Markers
@pytest.mark.unit
@pytest.mark.api
def test_health_endpoint(client):
response = client.get("/health")
assert response.status_code == 200
Async Tests
@pytest.mark.asyncio
async def test_async_function():
result = await some_async_function()
assert result is not None
Mocking
from unittest.mock import patch, MagicMock
def test_with_mock():
with patch('module.function') as mock_func:
mock_func.return_value = "mocked"
result = call_function_that_uses_it()
assert result == "mocked"
Continuous Integration
These tests are designed to run in CI pipelines:
# Example GitHub Actions
- name: Run tests
run: |
docker exec scheduler pytest --cov=src --cov-report=xml
docker exec scheduler pytest --cov=src --cov-report=html
Test Database
Tests use mocked database connections by default. For integration tests that require a real database:
- Set up a test database
- Use
POSTGRES_DB=test_schedulerenvironment variable - Run migrations before tests
- Clean up after tests
Troubleshooting
Tests fail with "ModuleNotFoundError"
# Install test dependencies
docker exec scheduler /venv/bin/pip install -r requirements.txt
Tests fail with database errors
- Tests use mocked connections by default
- Check that mocks are properly set up in conftest.py
- For integration tests, ensure test database exists
Coverage not working
# Reinstall pytest-cov
docker exec scheduler /venv/bin/pip install --upgrade pytest-cov
Best Practices
- Fast by default: Unit tests should be fast (<1s each)
- Isolated: Tests should not depend on each other
- Descriptive: Test names should clearly describe what they test
- Single assertion: Test one thing per test when possible
- Use fixtures: Reuse common setup via fixtures
- Mock external deps: Don't hit real databases/APIs in unit tests
- Mark appropriately: Use markers to categorize tests
Current Coverage
Run this to see current coverage:
docker exec scheduler pytest --cov=src --cov-report=term
Target: 80%+ code coverage for critical paths