Skip to main content
Keep unit tests fully mocked, use integration tests for filesystem and DB behavior, and reserve E2E for smoke-level network validation.

Test suite structure

tests/
├── conftest.py                        # shared fixtures for all layers
├── unit/                              # fully mocked — fast, no network or disk I/O
│   ├── cli/
│   │   ├── test_admin.py
│   │   ├── test_app_helpers.py
│   │   ├── test_banner.py
│   │   ├── test_display.py
│   │   ├── test_formatters.py
│   │   ├── test_runtime.py
│   │   └── test_source_resolution.py
│   ├── config/
│   ├── errors/
│   ├── llm/
│   ├── pipeline/
│   ├── storage/
│   ├── ui/
│   ├── utils/
│   └── youtube/
├── integration/                       # real SQLite & filesystem — no network
│   ├── cli/
│   ├── pipeline/
│   └── storage/
└── e2e/                               # real YouTube network calls (smoke tests)
    └── test_public_smoke.py

Running tests

# Full suite (parallel)
make test
uv run python -m pytest -n auto --dist=loadfile

# Unit tests only
make test-unit
uv run python -m pytest tests/unit -n auto

# Integration tests only
make test-integration
uv run python -m pytest tests/integration

# E2E (requires network + API key)
uv run pytest tests/e2e -v

# Specific file
uv run pytest tests/unit/youtube/test_parser.py -v

# Specific test
uv run pytest tests/unit/youtube/test_parser.py::test_extract_video_id_standard -v
make test-unit
uv run pytest tests/unit/cli -q
uv run pytest tests/unit/pipeline -q

Coverage

CI requires ≥ 90% coverage on the unit test suite.
uv run python -m pytest tests/unit \
  --cov=src/notewise \
  --cov-report=term-missing

uv run python -m pytest tests/unit \
  --cov=src/notewise \
  --cov-report=html
open htmlcov/index.html
Treat term-missing output as the primary signal when closing coverage gaps.

Writing unit tests

Conventions

  • Test files: test_*.py
  • Test classes: Test*
  • Test functions: test_*
  • All unit tests must mock external I/O — no real network calls, no real filesystem writes (use tmp_path for disk operations)

Mocking external calls

def test_fetch_transcript(mocker):
    mock_fetch = mocker.patch(
        "notewise.youtube.transcript.fetch_transcript",
        return_value=mock_transcript,
    )
    ...
    mock_fetch.assert_called_once_with("VIDEO_ID", languages=["en"])

Async tests

No special decorator needed (asyncio_mode = "auto"):
async def test_pipeline_run(mocker):
    mocker.patch("notewise.pipeline._execution.fetch_transcript", ...)
    result = await pipeline.run(["VIDEO_ID"])
    assert result.success_count == 1

Shared fixtures

Fixtures shared across the whole suite live in tests/conftest.py. Module-level fixtures used in only one file stay in that file.

Writing integration tests

Integration tests use real SQLite databases (via tmp_path) and the real filesystem. They must not make network calls.
def test_repository_lifecycle(tmp_path):
    db_path = tmp_path / ".notewise_cache.db"
    repo = DatabaseRepository(db_path)
    ...

Writing E2E tests

E2E tests make real YouTube requests. They should:
  • Only use publicly accessible videos
  • Be designed to pass with no LLM API key (test URL parsing and transcript fetching only)
  • Be marked as smoke tests
# tests/e2e/test_public_smoke.py
def test_public_video_transcript_fetch():
    ...  # uses a known-stable public video
Do not add flaky E2E cases that depend on unstable videos or strict timing assumptions.

pytest configuration

From pyproject.toml:
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]
addopts = "-ra"