# Testing Guide ## Overview Headquarter has comprehensive testing for both backend and frontend to ensure reliability and catch regressions. ## Backend Testing ### Test Framework - **pytest**: Test runner - **pytest-asyncio**: Async test support - **httpx**: HTTP client for API tests - **factory-boy**: Test data generation (recommended) ### Test Structure ``` apps/api/tests/ ├── conftest.py # Shared fixtures ├── test_auth_api.py # Auth endpoint tests ├── test_auth_services.py # Auth service tests ├── test_models.py # Database model tests ├── test_projects_api.py # Project endpoint tests ├── test_git_repositories.py # Repository tests └── ... ``` ### Running Tests ```bash cd apps/api # Run all tests pytest # Run with coverage pytest --cov=src --cov-report=html # Run specific test file pytest tests/test_auth_api.py # Run specific test pytest tests/test_auth_api.py::test_login_redirect # Run with verbose output pytest -v # Run async tests pytest --asyncio-mode=auto ``` ### Writing Tests **Unit Test Example**: ```python import pytest from src.utils.git_url_parser import extract_base_repo_url def test_extract_github_url(): url = "https://github.com/user/repo/tree/main" result = extract_base_repo_url(url) assert result == "https://github.com/user/repo.git" def test_valid_git_url_unchanged(): url = "https://github.com/user/repo.git" result = extract_base_repo_url(url) assert result == url ``` **Async Test Example**: ```python import pytest from httpx import AsyncClient @pytest.mark.asyncio async def test_get_projects(client: AsyncClient): response = await client.get("/projects") assert response.status_code == 200 assert isinstance(response.json(), list) ``` **API Integration Test**: ```python @pytest.mark.asyncio async def test_create_project(client: AsyncClient, auth_headers): response = await client.post( "/projects", json={"name": "Test Project"}, headers=auth_headers ) assert response.status_code == 201 data = response.json() assert data["name"] == "Test Project" assert "id" in data ``` ### Test Fixtures **conftest.py** provides: ```python import pytest from httpx import AsyncClient @pytest.fixture async def client(): from src.main import app async with AsyncClient(app=app, base_url="http://test") as client: yield client @pytest.fixture async def auth_headers(client): # Login and return auth headers response = await client.post("/auth/login") # ... setup session return {"Cookie": "session=..."} ``` ### Test Database Tests use a separate database: ```bash # Test database URL (from .env) TEST_DATABASE_URL=postgresql+asyncpg://headquarter:headquarter@localhost:5432/headquarter_test # Run tests with test DB TEST_DATABASE_URL=... pytest ``` ## Frontend Testing ### Test Framework - **Vitest**: Test runner - **React Testing Library**: Component testing - **jsdom**: DOM environment ### Test Structure ``` apps/web/src/ ├── components/ │ └── protected-route.test.tsx ├── pages/ │ ├── dashboard.test.tsx │ └── projects.test.tsx └── test/ └── setup.ts # Test setup ``` ### Running Tests ```bash cd apps/web # Run all tests npm run test # Run in watch mode npm run test -- --watch # Run with coverage npm run test -- --coverage # Run specific file npm run test -- protected-route ``` ### Writing Tests **Component Test Example**: ```typescript import { render, screen, fireEvent } from '@testing-library/react'; import { FileTree } from '../components/file-tree'; describe('FileTree', () => { const mockFiles = [ { name: 'src', type: 'directory', path: 'src' }, { name: 'main.py', type: 'file', path: 'src/main.py' }, ]; it('renders file list', () => { render( {}} />); expect(screen.getByText('src')).toBeInTheDocument(); expect(screen.getByText('main.py')).toBeInTheDocument(); }); it('calls onFileClick when file clicked', () => { const handleClick = vi.fn(); render(); fireEvent.click(screen.getByText('main.py')); expect(handleClick).toHaveBeenCalledWith(mockFiles[1]); }); }); ``` **Async Test Example**: ```typescript import { render, screen, waitFor } from '@testing-library/react'; import { ProjectsPage } from '../pages/projects'; describe('ProjectsPage', () => { it('loads and displays projects', async () => { render(); await waitFor(() => { expect(screen.getByText('My Projects')).toBeInTheDocument(); }); }); }); ``` ## E2E Testing (Future) ### Playwright Setup ```bash cd apps/web npm install -D @playwright/test npx playwright install ``` **Example E2E Test**: ```typescript import { test, expect } from '@playwright/test'; test('user can login', async ({ page }) => { await page.goto('http://localhost:5173'); await page.click('text=Login'); // Authentik login await page.fill('[name="username"]', 'test@example.com'); await page.fill('[name="password"]', 'password'); await page.click('text=Sign In'); // Should redirect back to app await expect(page).toHaveURL('http://localhost:5173/dashboard'); }); ``` ## Test Data ### Factories (Recommended) Use factory-boy for test data: ```python # tests/factories.py import factory from src.models.user import User class UserFactory(factory.Factory): class Meta: model = User email = factory.Faker('email') name = factory.Faker('name') authentik_id = factory.Faker('uuid4') ``` ### Fixtures ```python @pytest.fixture async def test_user(db_session): user = UserFactory() db_session.add(user) await db_session.commit() return user ``` ## Coverage Goals | Component | Target Coverage | |-----------|----------------| | Backend API | 80%+ | | Backend Services | 90%+ | | Backend Models | 90%+ | | Frontend Components | 70%+ | | Frontend Pages | 60%+ | ## Continuous Integration ### GitHub Actions (Example) ```yaml name: Tests on: [push, pull_request] jobs: backend: runs-on: ubuntu-latest services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: headquarter steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' - run: pip install -e ".[dev]" - run: pytest --cov=src --cov-report=xml frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: cd apps/web && npm ci - run: cd apps/web && npm run test ``` ## Best Practices ### Backend 1. **Test isolation**: Each test should be independent 2. **Use fixtures**: Don't repeat setup code 3. **Mock external services**: Authentik, git operations 4. **Test edge cases**: Empty lists, invalid inputs, errors 5. **Async properly**: Use `pytest.mark.asyncio` and async fixtures ### Frontend 1. **Test behavior, not implementation**: Check what user sees 2. **Use data-testid**: For stable selectors 3. **Mock API calls**: Don't hit real backend 4. **Test accessibility**: Use `screen.getByRole` 5. **Snapshot sparingly**: Only for complex UIs ## Debugging Tests ### Backend ```bash # Run with debugger pytest --pdb # Run specific test with verbose pytest -v -s test_file.py::test_name # Show print statements pytest -s ``` ### Frontend ```bash # Debug mode npm run test -- --reporter=verbose # Show browser (for E2E) npx playwright test --headed ``` ## Resources - [pytest Documentation](https://docs.pytest.org/) - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) - [Vitest Documentation](https://vitest.dev/) - [Playwright Documentation](https://playwright.dev/)