1. `executor.max_concurrent` has never existed. Concurrency is capped by the
module-level MAX_CONCURRENT_TASKS constant via asyncio.Semaphore(
MAX_CONCURRENT_TASKS) in TaskExecutor.__init__ — confirmed with git log -p
across this file's whole history (three commits), the name has always
been the module constant, never an instance attribute.
test_executor_initialization now asserts MAX_CONCURRENT_TASKS == 5 and the
semaphore's initial count, instead of a name the class never had.
test_concurrent_task_limit asserted `mock_execute.call_count <=
executor.max_concurrent`, which — separately from the AttributeError — was
asserting the wrong observable: process_minute() awaits the full batch via
asyncio.gather before returning, so by the time the assertion runs all 10
scheduled tasks have executed; the semaphore bounds how many run
concurrently mid-flight, not the eventual call_count. Reworded to assert
all scheduled tasks still run (call_count == len(tasks)); a concurrency-
in-flight assertion would need a task that can be observed mid-execution,
which the AsyncMock stand-in does not provide.
2. _run_executor (src/tasks/executor.py) loads the executor module with the
__import__ builtin directly (`__import__(module_path, fromlist=
['execute'])`), not importlib.import_module — this repo's own CLAUDE.md
documents it as "the thing that will mislead you" about this module.
importlib is never imported there, so patch('src.tasks.executor.importlib.
import_module') failed at patch setup, before the three
test_execute_task_* bodies ran at all. Switched to patch('builtins.
__import__', side_effect=...) with a routing function that falls through
to the real import for anything other than the target module — verified
the call-recording shape empirically first (call('name', fromlist=[...])).
Source is unchanged in both cases; both are test-only defects present since
this file's initial commit.
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