Skip to content

Testing

VDL has comprehensive test coverage: unit, contract, integration, and smoke tests.

Test Layout

tests/
  conftest.py                    # Global fixtures (HOME isolation)
  README.md

  unit/                          # Per-module unit tests (~40 files)
    test_client.py               # VDLClient behavior
    test_cli.py                  # CLI commands and flags
    test_runtime.py              # Wiring tiers
    test_models.py               # Dataclass validation
    ... (and more)

  contract/                      # Interface compliance
    test_playlist_contract.py     # PlaylistResolver protocol

  integration/                   # Real SQLite, real filesystem
    test_sqlite_state.py         # StateStore persistence
    test_playlist_partial_failure.py  # Batch failure isolation

  smoke/                         # Live network tests (marked slow)
    test_install.sh              # Installer validation

  support/                       # Test utilities
    fake_downloader.py           # FakeDownloader mock
    fake_playlist_resolver.py     # FakePlaylistResolver mock
    fake_state_store.py          # InMemoryStateStore

Running Tests

# All tests (except slow/smoke)
PYTHONPATH=src uv run pytest -q

# With slow tests
PYTHONPATH=src uv run pytest -q

# Specific test file
PYTHONPATH=src uv run pytest tests/unit/test_client.py -v

# Specific test function
PYTHONPATH=src uv run pytest tests/unit/test_client.py::TestDownload::test_completed -v

# Run only contract tests
PYTHONPATH=src uv run pytest tests/contract/ -v

HOME Isolation

The autouse fixture in conftest.py redirects HOME to a temporary directory:

# tests/conftest.py
@pytest.fixture(autouse=True)
def _isolate_home(tmp_path, monkeypatch):
    monkeypatch.setenv("HOME", str(tmp_path))

Benefits: - Tests never touch real ~/.config/vdl/ or ~/.local/state/vdl/ - Tests are isolated from each other - Temporary directories are auto-cleaned

Fake Implementations

FakeDownloader

Configurable per-URL results:

from tests.support.fake_downloader import FakeDownloader
from vdl.models import DownloadStatus, Quality

downloader = FakeDownloader(
    results={
        "https://example.com/video1": DownloadResult(
            id="test1",
            source_url="https://example.com/video1",
            status=DownloadStatus.COMPLETED,
            quality=Quality.P720,
            output_path="/path/to/video1.mp4",
            error=None,
            skip_reason=None,
            metadata={}
        )
    }
)

result = await downloader.download(request, on_progress=None)

FakePlaylistResolver

from tests.support.fake_playlist_resolver import FakePlaylistResolver

resolver = FakePlaylistResolver(
    playlists={
        "https://example.com/playlist": PlaylistInfo(
            url="https://example.com/playlist",
            title="Test Playlist",
            items=[
                PlaylistItem(url="...", id="1", title="Item 1", index=1, duration_seconds=180, selected=True, metadata={}),
                PlaylistItem(url="...", id="2", title="Item 2", index=2, duration_seconds=240, selected=True, metadata={}),
            ],
            metadata={}
        )
    }
)

playlist = await resolver.resolve("https://example.com/playlist")
assert len(playlist.items) == 2

InMemoryStateStore

from tests.support.fake_state_store import InMemoryStateStore

state = InMemoryStateStore()
client = VDLClient(state_store=state)

result = await client.download("URL")
assert len(state.list_recent()) == 1

Async Tests

VDL uses pytest-asyncio. Tests are defined with async def:

import pytest

@pytest.mark.asyncio
async def test_download():
    client = VDLClient()
    result = await client.download("https://example.com/video")
    assert result.status == DownloadStatus.COMPLETED

If asyncio_mode = "auto" in pyproject.toml, the @pytest.mark.asyncio decorator is optional on async test functions.

Contract Tests

Contract tests validate that fake implementations behave identically to real ones:

# tests/contract/test_playlist_contract.py
class TestPlaylistItemsToRequests:
    async def test_all_selected_by_default(self):
        playlist = PlaylistInfo(
            url="...",
            title="...",
            items=[PlaylistItem(...) for _ in range(3)],
            metadata={}
        )
        requests = playlist_items_to_requests(playlist, selected_indexes=None, ...)
        assert len(requests) == 3
        assert all(r.selected for r in requests)

These tests pass with both fake and real implementations, ensuring compatibility.

Unit Test Structure

Example from test_client.py:

class TestDownload:
    @pytest.mark.asyncio
    async def test_completed(self, client):
        result = await client.download("https://example.com/video")
        assert result.status == DownloadStatus.COMPLETED
        assert result.output_path is not None

    @pytest.mark.asyncio
    async def test_skipped(self, client, state):
        # First download
        await client.download("URL")
        # Second download (collision)
        result = await client.download("URL", if_exists=IfExists.SKIP)
        assert result.status == DownloadStatus.SKIPPED

    @pytest.mark.asyncio
    async def test_failed_no_downloader(self):
        client = VDLClient(downloader=None)
        with pytest.raises(VDLError):
            await client.download("URL")

Integration Tests

Real SQLite and filesystem:

# tests/integration/test_sqlite_state.py
class TestSQLiteStateStore:
    @pytest.mark.asyncio
    async def test_record_and_retrieve(self, tmp_path):
        db_path = tmp_path / "test.db"
        state = SQLiteStateStore(db_path=db_path)

        await state.record_completed(
            id="test1",
            result=DownloadResult(...)
        )

        record = await state.get_status("test1")
        assert record.status == DownloadStatus.COMPLETED

Adding a New Test

  1. Create tests/unit/test_<module>.py or add to existing file
  2. Use descriptive class and function names
  3. Inject fixtures (client, state, downloader, etc.)
  4. Test both success and failure paths
  5. Run: PYTHONPATH=src uv run pytest tests/unit/test_<module>.py -v

Example:

# tests/unit/test_my_feature.py
import pytest
from vdl import VDLClient
from vdl.models import DownloadStatus

class TestMyFeature:
    @pytest.mark.asyncio
    async def test_basic_behavior(self):
        client = VDLClient()
        result = await client.download("https://example.com/video")
        assert result.status == DownloadStatus.COMPLETED

    @pytest.mark.asyncio
    async def test_error_handling(self):
        client = VDLClient(downloader=None)
        with pytest.raises(VDLError):
            await client.download("URL")

CI Test Jobs

GitHub/GitLab CI runs: - vdl-test — full test suite (required for release) - vdl-test-ytdlp-latest — with latest stable yt-dlp (advisory) - vdl-test-ytdlp-nightly — with nightly yt-dlp (optional)

See .gitlab-ci.yml for the pipeline definition.

See Also