Files
scheduler/tests
jpmschweitzer 68bea0cceb fix(tests): correct two independent AttributeErrors in test_task_executor
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.
2026-08-18 15:49:37 +02:00
..

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.html in 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:

  1. Set up a test database
  2. Use POSTGRES_DB=test_scheduler environment variable
  3. Run migrations before tests
  4. 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

  1. Fast by default: Unit tests should be fast (<1s each)
  2. Isolated: Tests should not depend on each other
  3. Descriptive: Test names should clearly describe what they test
  4. Single assertion: Test one thing per test when possible
  5. Use fixtures: Reuse common setup via fixtures
  6. Mock external deps: Don't hit real databases/APIs in unit tests
  7. 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