Testing Guide

Testing Guide

Premium Documentation

This guide covers the SkySpy test suite architecture, running tests, writing new tests, and best practices for maintaining comprehensive test coverage.


Testing Overview

SkySpy employs a multi-layered testing strategy to ensure reliability across the entire stack.

flowchart TB
    subgraph Testing["Test Pyramid"]
        E2E["E2E Tests<br/>User Workflows"]
        INT["Integration Tests<br/>Component Interactions"]
        UNIT["Unit Tests<br/>Individual Functions"]
    end

    E2E --> INT
    INT --> UNIT

    style E2E fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff
    style INT fill:#feca57,stroke:#333,stroke-width:2px,color:#333
    style UNIT fill:#48dbfb,stroke:#333,stroke-width:2px,color:#333
LayerFrameworkLocationPurpose
Unit Testspytest + pytest-djangoskyspy_django/skyspy/tests/Test individual functions, methods, and classes
Integration Testspytest + pytest-asyncioskyspy_django/skyspy/tests/Test component interactions and data flow
Backend E2Epytest + Django Channelsskyspy_django/skyspy/tests/e2e/Test Socket.IO consumers and real-time features
Frontend E2EPlaywrightweb/e2e/Test user workflows in the browser

Coverage Targets

Coverage Goals:

  • 80%+ Backend Coverage
  • 100% Critical Paths
  • All User Workflows

Coverage Requirements

  • Backend: Minimum 80% line coverage
  • Critical Paths: 100% coverage for authentication, alerts, and safety features
  • Frontend E2E: Cover all major user workflows

Test Structure and Organization

Backend Tests

skyspy_django/skyspy/tests/
├── conftest.py                    # Shared fixtures for all tests
├── factories.py                   # Factory Boy model factories
├── e2e/
│   ├── conftest.py               # E2E-specific fixtures
│   ├── test_e2e_stats.py         # Statistics E2E tests
│   └── test_e2e_socketio.py      # Socket.IO E2E tests
├── test_api_*.py                 # REST API endpoint tests
├── test_consumers_*.py           # Socket.IO consumer tests
├── test_services_*.py            # Business logic/service tests
├── test_tasks_*.py               # Celery task tests
├── test_integration.py           # Cross-component integration tests
└── test_settings.py              # Configuration tests

Frontend E2E Tests

web/e2e/
├── fixtures/
│   └── test-setup.js             # Mock data generators and test utilities
├── tests/
│   ├── alerts.spec.js            # Alert management tests
│   ├── map.spec.js               # Map and aircraft display tests
│   └── ...                       # Additional feature tests
└── playwright.config.js          # Playwright configuration

Naming Conventions

PatternDescriptionExample
test_api_*.pyAPI endpoint teststest_api_aircraft.py
test_consumers_*.pySocket.IO consumer teststest_consumers_aircraft.py
test_services_*.pyService layer teststest_services_alerts.py
test_tasks_*.pyCelery task teststest_tasks_airspace.py
test_e2e_*.pyBackend E2E teststest_e2e_socket_io.py
*.spec.jsFrontend E2E testsmap.spec.js

Running Tests

Prerequisites

Setup Required

Ensure you have the test dependencies installed before running tests.

Backend Setup:

# Backend dependencies
cd skyspy_django
pip install -e ".[test]"

Frontend Setup:

# Frontend dependencies
cd web
npm install
npx playwright install

Unit Tests

Run All Tests:

cd skyspy_django
pytest

Specific Module:

pytest skyspy/tests/test_api_aircraft.py

Pattern Matching:

pytest -k "aircraft"

Verbose Output:

pytest -v

Integration Tests

# Run integration tests
pytest skyspy/tests/test_integration.py

# Run async integration tests
pytest skyspy/tests/test_integration.py -v --asyncio-mode=auto

E2E Tests

Backend E2E (Socket.IO/Channels)

Tests real-time Socket.IO functionality and Django Channels consumers.

All Backend E2E:

pytest skyspy/tests/e2e/ -v

Specific E2E Test:

pytest skyspy/tests/e2e/test_e2e_socket_io.py::test_aircraft_position_updates -v

Frontend E2E (Playwright)

Tests user workflows in real browser environments.

All Frontend E2E:

cd web
npx playwright test

Specific Test File:

npx playwright test tests/map.spec.js

Headed Mode:

npx playwright test --headed

UI Mode (Debug):

npx playwright test --ui

Docker-Based Testing

docker-compose -f docker-compose.test.yaml up --build --abort-on-container-exit

Docker Test Environment Includes:

  • Isolated PostgreSQL database
  • Redis for caching and Channels
  • Proper network configuration
  • Coverage report generation

Test Fixtures and Factories

Backend Fixtures (conftest.py)

SkySpy uses pytest fixtures for test setup. Key fixtures are defined in skyspy_django/skyspy/tests/conftest.py:

Database Fixtures

Manage database state and provide API clients for testing.

@pytest.fixture
def db_session(db):
    """Provides a database session with automatic rollback."""
    yield
    # Automatic rollback after test

@pytest.fixture
def api_client():
    """Returns a DRF APIClient for testing API endpoints."""
    from rest_framework.test import APIClient
    return APIClient()

@pytest.fixture
def authenticated_client(api_client, user):
    """Returns an authenticated API client."""
    api_client.force_authenticate(user=user)
    return api_client

Socket.IO Fixtures

Enable testing of real-time Socket.IO functionality.

@pytest.fixture
async def communicator(application):
    """Socket.IO communicator for testing consumers."""
    from channels.testing import WebsocketCommunicator
    communicator = WebsocketCommunicator(application, "/socket.io/")
    connected, _ = await communicator.connect()
    assert connected
    yield communicator
    await communicator.disconnect()

User Fixtures

Create test users with various permission levels.

@pytest.fixture
def user(db):
    """Creates a standard test user."""
    from skyspy.models import User
    return User.objects.create_user(
        username="testuser",
        email="[email protected]",
        password="testpass123"
    )

@pytest.fixture
def admin_user(db):
    """Creates an admin user with elevated permissions."""
    from skyspy.models import User
    return User.objects.create_superuser(
        username="admin",
        email="[email protected]",
        password="adminpass123"
    )

Factory Boy Factories (factories.py)

Factories generate realistic test data using Factory Boy:

import factory
from factory.django import DjangoModelFactory
from skyspy.models import Aircraft, Alert, User

class UserFactory(DjangoModelFactory):
    class Meta:
        model = User

    username = factory.Sequence(lambda n: f"user{n}")
    email = factory.LazyAttribute(lambda obj: f"{obj.username}@example.com")
    password = factory.PostGenerationMethodCall("set_password", "testpass123")

class AircraftFactory(DjangoModelFactory):
    class Meta:
        model = Aircraft

    icao_hex = factory.Sequence(lambda n: f"A{n:05X}")
    callsign = factory.Sequence(lambda n: f"TEST{n:03d}")
    latitude = factory.Faker("latitude")
    longitude = factory.Faker("longitude")
    altitude = factory.Faker("random_int", min=1000, max=45000)
    speed = factory.Faker("random_int", min=100, max=600)
    heading = factory.Faker("random_int", min=0, max=359)

class AlertFactory(DjangoModelFactory):
    class Meta:
        model = Alert

    user = factory.SubFactory(UserFactory)
    name = factory.Sequence(lambda n: f"Alert Rule {n}")
    conditions = factory.LazyFunction(lambda: {
        "field": "altitude",
        "operator": "less_than",
        "value": 5000
    })
    is_active = True

Using Factories in Tests

Factories simplify test data creation and make tests more readable.

def test_aircraft_list(authenticated_client):
    # Create test aircraft
    aircraft = AircraftFactory.create_batch(5)

    response = authenticated_client.get("/api/v1/aircraft/")

    assert response.status_code == 200
    assert len(response.json()["results"]) == 5

def test_alert_triggers(user):
    alert = AlertFactory(user=user, conditions={
        "field": "altitude",
        "operator": "less_than",
        "value": 3000
    })
    aircraft = AircraftFactory(altitude=2500)

    # Test alert evaluation logic
    assert alert.evaluate(aircraft) is True

Frontend Test Fixtures (test-setup.js)

Frontend E2E tests use custom fixtures for mock data and API mocking:

Mock Aircraft:

// Generate mock aircraft data
export function generateMockAircraft(count = 10) {
    return Array.from({ length: count }, (_, i) => ({
        icao_hex: `A${String(i).padStart(5, '0')}`,
        callsign: `TEST${String(i).padStart(3, '0')}`,
        latitude: 40.7128 + (Math.random() - 0.5) * 2,
        longitude: -74.0060 + (Math.random() - 0.5) * 2,
        altitude: Math.floor(Math.random() * 40000) + 5000,
        speed: Math.floor(Math.random() * 500) + 100,
        heading: Math.floor(Math.random() * 360),
        aircraft_type: 'B738',
        registration: `N${100 + i}AA`
    }));
}

Mock ACARS:

// Generate mock ACARS messages
export function generateMockAcarsMessages(count = 5) {
    return Array.from({ length: count }, (_, i) => ({
        id: i + 1,
        timestamp: new Date(Date.now() - i * 60000).toISOString(),
        icao_hex: `A${String(i).padStart(5, '0')}`,
        message: `ACARS message content ${i}`,
        label: 'H1',
        block_id: String.fromCharCode(65 + i)
    }));
}

Mock Alerts:

// Generate mock alert rules
export function generateMockAlertRules(count = 3) {
    return Array.from({ length: count }, (_, i) => ({
        id: i + 1,
        name: `Alert Rule ${i + 1}`,
        conditions: [{ field: 'altitude', operator: 'lt', value: 5000 }],
        is_active: true,
        notification_channels: ['browser']
    }));
}

Extended Playwright Test Fixture

import { test as base, expect } from '@playwright/test';

export const test = base.extend({
    // Mock API routes
    mockApi: async ({ page }, use) => {
        const mocks = {
            aircraft: generateMockAircraft(20),
            alerts: generateMockAlertRules(3),
            acars: generateMockAcarsMessages(10)
        };

        await page.route('**/api/v1/aircraft/**', route => {
            route.fulfill({ json: { results: mocks.aircraft } });
        });

        await page.route('**/api/v1/alerts/**', route => {
            route.fulfill({ json: { results: mocks.alerts } });
        });

        await use(mocks);
    },

    // Socket.IO mock
    socketMock: async ({ page }, use) => {
        const socketMock = new SocketIOMock();
        await page.exposeFunction('__socketMockEmit', (event, data) => {
            socketMock.emit(event, data);
        });
        await use(socketMock);
    },

    // Helper utilities
    helpers: async ({ page }, use) => {
        await use({
            waitForMapLoad: () => page.waitForSelector('.mapboxgl-canvas'),
            waitForAircraftMarkers: () => page.waitForSelector('[data-testid="aircraft-marker"]'),
            clickAircraft: (icao) => page.click(`[data-aircraft-id="${icao}"]`)
        });
    }
});

Mocking Strategies

Backend Mocking

flowchart LR
    subgraph Test["Test"]
        TC[Test Code]
    end

    subgraph Mocks["Mocks"]
        ME[External Services]
        MC[Cache/Redis]
        MT[Celery Tasks]
    end

    TC --> ME
    TC --> MC
    TC --> MT

    style Test fill:#48dbfb,stroke:#333,stroke-width:2px
    style Mocks fill:#feca57,stroke:#333,stroke-width:2px

Mocking External Services

Isolate tests from external API dependencies.

from unittest.mock import patch, MagicMock

def test_external_api_call():
    with patch("skyspy.services.external.fetch_weather") as mock_weather:
        mock_weather.return_value = {"temperature": 72, "conditions": "clear"}

        result = get_flight_conditions("KJFK")

        mock_weather.assert_called_once_with("KJFK")
        assert result["conditions"] == "clear"

Mocking Redis/Cache

Control cache behavior in tests.

@pytest.fixture
def mock_cache():
    with patch("django.core.cache.cache") as mock:
        mock.get.return_value = None
        mock.set.return_value = True
        yield mock

def test_cached_aircraft_data(mock_cache):
    mock_cache.get.return_value = {"icao": "ABC123", "cached": True}

    result = get_aircraft_data("ABC123")

    assert result["cached"] is True

Mocking Celery Tasks

Test task scheduling without running background workers.

from unittest.mock import patch

def test_task_scheduling():
    with patch("skyspy.tasks.process_aircraft.delay") as mock_task:
        mock_task.return_value.id = "test-task-id"

        schedule_aircraft_processing(["ABC123", "DEF456"])

        assert mock_task.call_count == 2

Frontend Mocking

API Route Mocking with Playwright

Intercept and mock API calls in browser tests.

test('displays aircraft list', async ({ page }) => {
    // Mock the aircraft API endpoint
    await page.route('**/api/v1/aircraft/', async route => {
        await route.fulfill({
            status: 200,
            contentType: 'application/json',
            body: JSON.stringify({
                results: [
                    { icao_hex: 'ABC123', callsign: 'TEST001', altitude: 35000 },
                    { icao_hex: 'DEF456', callsign: 'TEST002', altitude: 28000 }
                ],
                count: 2
            })
        });
    });

    await page.goto('/');
    await expect(page.locator('[data-testid="aircraft-list"]')).toContainText('TEST001');
});

Socket.IO Mocking

Simulate real-time updates in browser tests.

class SocketIOMock {
    constructor() {
        this.handlers = new Map();
        this.messages = [];
    }

    on(event, handler) {
        this.handlers.set(event, handler);
    }

    emit(event, data) {
        const handler = this.handlers.get(event);
        if (handler) handler(data);
    }

    send(data) {
        this.messages.push(data);
    }

    simulateAircraftUpdate(aircraft) {
        this.emit('aircraft:update', aircraft);
    }
}

test('receives real-time aircraft updates', async ({ page, socketMock }) => {
    await page.goto('/');

    // Simulate incoming Socket.IO message
    socketMock.simulateAircraftUpdate({
        hex: 'ABC123',
        lat: 40.7128,
        lon: -74.0060,
        alt_baro: 35000
    });

    await expect(page.locator('[data-aircraft-id="ABC123"]')).toBeVisible();
});

Mocking Authentication State

Test authenticated user flows without real login.

test('authenticated user sees alerts', async ({ page }) => {
    // Set auth token in storage before navigation
    await page.addInitScript(() => {
        localStorage.setItem('auth_token', 'test-jwt-token');
        localStorage.setItem('user', JSON.stringify({
            id: 1,
            username: 'testuser',
            permissions: ['view_alerts', 'create_alerts']
        }));
    });

    await page.goto('/alerts');
    await expect(page.locator('h1')).toContainText('Alert Rules');
});

Coverage Reporting

Backend Coverage

Generate Coverage:

cd skyspy_django
pytest --cov=skyspy --cov-report=html --cov-report=xml

View HTML Report:

# Generate and open HTML report
pytest --cov=skyspy --cov-report=html
open htmlcov/index.html

Generate Badge:

# Generate coverage badge
coverage-badge -o coverage.svg -f

Coverage Report Locations:

  • HTML Report: htmlcov/index.html (open in browser)
  • XML Report: coverage.xml (for CI integration)
  • Terminal Summary: Displayed after test run

Coverage Configuration (pyproject.toml)

[tool.coverage.run]
source = ["skyspy"]
omit = [
    "*/migrations/*",
    "*/tests/*",
    "*/__pycache__/*",
    "*/admin.py"
]
branch = true

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise NotImplementedError",
    "if TYPE_CHECKING:",
    "if settings.DEBUG"
]
fail_under = 80
show_missing = true

CI/CD Integration

flowchart LR
    subgraph Trigger["Triggers"]
        PUSH[Push to main/develop]
        PR[Pull Request]
    end

    subgraph Jobs["Jobs"]
        BE[Backend Tests]
        FE[Frontend E2E]
    end

    subgraph Services["Services"]
        PG[(PostgreSQL)]
        RD[(Redis)]
    end

    subgraph Output["Output"]
        COV[Coverage Report]
        ART[Test Artifacts]
    end

    PUSH --> BE
    PUSH --> FE
    PR --> BE
    PR --> FE

    BE --> PG
    BE --> RD
    BE --> COV

    FE --> ART

    style Trigger fill:#48dbfb,stroke:#333,stroke-width:2px
    style Jobs fill:#feca57,stroke:#333,stroke-width:2px
    style Services fill:#ff6b6b,stroke:#333,stroke-width:2px
    style Output fill:#26de81,stroke:#333,stroke-width:2px

GitHub Actions Workflow

SkySpy uses GitHub Actions for continuous integration. The test workflow is triggered on:

  • Push to main or develop branches
  • Pull requests targeting main

Backend Tests:

name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  backend-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: skyspy_test
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          cd skyspy_django
          pip install -e ".[test]"

      - name: Run tests with coverage
        run: |
          cd skyspy_django
          pytest --cov=skyspy --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: skyspy_django/coverage.xml

Frontend E2E:

  frontend-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: |
          cd web
          npm ci
          npx playwright install --with-deps

      - name: Run E2E tests
        run: |
          cd web
          npx playwright test

      - name: Upload test artifacts
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: web/playwright-report/

Pre-commit Hooks

pip install pre-commit
pre-commit install

Pre-commit configuration (.pre-commit-config.yaml):

repos:
  - repo: local
    hooks:
      - id: pytest-check
        name: pytest-check
        entry: pytest skyspy_django/skyspy/tests/ -x -q
        language: system
        pass_filenames: false
        always_run: true

Local CI Simulation

docker-compose -f docker-compose.test.yaml up --build

Writing New Tests

Backend Unit Test Template

import pytest
from skyspy.tests.factories import AircraftFactory, UserFactory

class TestAircraftService:
    """Tests for the Aircraft service layer."""

    @pytest.fixture
    def service(self):
        from skyspy.services.aircraft import AircraftService
        return AircraftService()

    def test_get_aircraft_by_icao(self, db, service):
        """Test retrieving aircraft by ICAO hex code."""
        aircraft = AircraftFactory(icao_hex="ABC123")

        result = service.get_by_icao("ABC123")

        assert result is not None
        assert result.icao_hex == "ABC123"

    def test_get_aircraft_not_found(self, db, service):
        """Test behavior when aircraft is not found."""
        result = service.get_by_icao("NONEXISTENT")

        assert result is None

    def test_filter_by_altitude(self, db, service):
        """Test filtering aircraft by altitude range."""
        AircraftFactory(altitude=10000)
        AircraftFactory(altitude=20000)
        AircraftFactory(altitude=30000)

        result = service.filter_by_altitude(min_alt=15000, max_alt=25000)

        assert len(result) == 1
        assert result[0].altitude == 20000

Backend API Test Template

import pytest
from rest_framework import status
from skyspy.tests.factories import AircraftFactory, UserFactory

class TestAircraftAPI:
    """Tests for the Aircraft REST API."""

    @pytest.fixture
    def auth_client(self, api_client, db):
        user = UserFactory()
        api_client.force_authenticate(user=user)
        return api_client

    def test_list_aircraft(self, auth_client):
        """Test listing all aircraft."""
        AircraftFactory.create_batch(3)

        response = auth_client.get("/api/v1/aircraft/")

        assert response.status_code == status.HTTP_200_OK
        assert len(response.json()["results"]) == 3

    def test_retrieve_aircraft(self, auth_client):
        """Test retrieving single aircraft."""
        aircraft = AircraftFactory(icao_hex="ABC123")

        response = auth_client.get(f"/api/v1/aircraft/{aircraft.id}/")

        assert response.status_code == status.HTTP_200_OK
        assert response.json()["icao_hex"] == "ABC123"

    def test_unauthenticated_access_denied(self, api_client):
        """Test that unauthenticated requests are rejected."""
        response = api_client.get("/api/v1/aircraft/")

        assert response.status_code == status.HTTP_401_UNAUTHORIZED

Backend Socket.IO Consumer Test Template

import pytest
from channels.testing import WebsocketCommunicator
from skyspy.channels.consumers.aircraft import AircraftConsumer

@pytest.mark.asyncio
class TestAircraftConsumer:
    """Tests for the Aircraft Socket.IO consumer."""

    @pytest.fixture
    async def communicator(self, application):
        communicator = WebsocketCommunicator(application, "/socket.io/")
        connected, _ = await communicator.connect()
        assert connected
        yield communicator
        await communicator.disconnect()

    async def test_subscribe_to_aircraft(self, communicator):
        """Test subscribing to aircraft updates."""
        await communicator.send_json_to({
            "type": "subscribe",
            "topics": ["aircraft"]
        })

        response = await communicator.receive_json_from()

        assert response["type"] == "subscribed"

    async def test_receive_position_update(self, communicator):
        """Test receiving aircraft position updates."""
        await communicator.send_json_to({
            "type": "subscribe",
            "topics": ["aircraft"]
        })

        # Simulate position update from channel layer
        await self.send_position_update("ABC123", lat=40.7128, lon=-74.0060)

        response = await communicator.receive_json_from(timeout=5)

        assert response["type"] == "aircraft:update"
        assert response["data"]["hex"] == "ABC123"

Frontend E2E Test Template

import { test, expect } from '../fixtures/test-setup';

test.describe('Aircraft Map Features', () => {
    test.beforeEach(async ({ page, mockApi }) => {
        // mockApi fixture automatically sets up API mocks
        await page.goto('/');
        await page.waitForSelector('.mapboxgl-canvas');
    });

    test('displays aircraft markers on map', async ({ page, helpers }) => {
        await helpers.waitForAircraftMarkers();

        const markers = await page.locator('[data-testid="aircraft-marker"]').count();
        expect(markers).toBeGreaterThan(0);
    });

    test('clicking aircraft shows detail panel', async ({ page }) => {
        await page.click('[data-testid="aircraft-marker"]');

        await expect(page.locator('[data-testid="aircraft-detail"]')).toBeVisible();
    });

    test('filter aircraft by altitude', async ({ page }) => {
        await page.click('[data-testid="filter-button"]');
        await page.fill('[data-testid="min-altitude"]', '10000');
        await page.fill('[data-testid="max-altitude"]', '20000');
        await page.click('[data-testid="apply-filter"]');

        // Verify filtered results
        const markers = await page.locator('[data-testid="aircraft-marker"]').all();
        for (const marker of markers) {
            const altitude = await marker.getAttribute('data-altitude');
            expect(parseInt(altitude)).toBeGreaterThanOrEqual(10000);
            expect(parseInt(altitude)).toBeLessThanOrEqual(20000);
        }
    });
});

Test Checklist

When writing new tests, ensure you follow these best practices:

Pre-Submit Checklist:

  • Use descriptive test names that explain the expected behavior
  • Follow the Arrange-Act-Assert pattern
  • Use factories for test data creation
  • Mock external dependencies
  • Test both success and error cases
  • Test edge cases and boundary conditions
  • Include proper cleanup in fixtures
  • Add appropriate test markers (@pytest.mark.slow, @pytest.mark.integration)
  • Verify test runs in isolation (no test interdependencies)

Common Pytest Markers

# Mark test as slow-running
@pytest.mark.slow
def test_large_dataset_processing():
    pass

# Mark test as requiring database
@pytest.mark.django_db
def test_database_operation():
    pass

# Mark test as async
@pytest.mark.asyncio
async def test_async_operation():
    pass

# Skip test conditionally
@pytest.mark.skipif(condition, reason="...")
def test_feature():
    pass

# Parametrize test with multiple inputs
@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input, expected):
    assert input * 2 == expected

Troubleshooting

Warning: Database Not Cleaned Between Tests

Ensure you're using the db fixture or @pytest.mark.django_db

@pytest.mark.django_db
def test_needs_database():
    # Database access works here
    pass

Warning: Socket.IO Tests Timing Out

Increase the timeout or check consumer connection logic

response = await communicator.receive_json_from(timeout=10)

Warning: Playwright Tests Flaky

Add explicit waits for elements

await page.waitForSelector('[data-testid="element"]', { state: 'visible' });

Warning: Coverage Not Detecting Async Code

Ensure pytest-asyncio is properly configured

[pytest]
asyncio_mode = auto

Debug Mode

Backend Debug:

pytest -v --tb=long --capture=no

Frontend Debug:

npx playwright test --debug

Quick Reference

TaskCommand
Run all backend testspytest
Run with coveragepytest --cov=skyspy
Run specific test filepytest path/to/test_file.py
Run matching testspytest -k "pattern"
Run frontend E2Enpx playwright test
Run E2E headednpx playwright test --headed
Run E2E with UInpx playwright test --ui
Docker test suitedocker-compose -f docker-compose.test.yaml up
Generate coverage HTMLpytest --cov=skyspy --cov-report=html
Debug backend testspytest -v --tb=long --capture=no
Debug frontend testsnpx playwright test --debug

Need Help?

For questions or issues with testing, please open an issue on the GitHub repository.