83f94b1f09
Add complete documentation structure: - Frontend architecture documentation - Database schema documentation - Deployment guides (Docker, Traefik, Authentik, Environment) - Development guides (Setup, Testing, Contributing, Quality Gates) - Deployment architecture documentation - Updated docs README with complete navigation All new features and APIs are now documented. Quality gates: docs only, no code changes
7.8 KiB
7.8 KiB
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
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:
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:
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:
@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:
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:
# 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
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:
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(<FileTree files={mockFiles} onFileClick={() => {}} />);
expect(screen.getByText('src')).toBeInTheDocument();
expect(screen.getByText('main.py')).toBeInTheDocument();
});
it('calls onFileClick when file clicked', () => {
const handleClick = vi.fn();
render(<FileTree files={mockFiles} onFileClick={handleClick} />);
fireEvent.click(screen.getByText('main.py'));
expect(handleClick).toHaveBeenCalledWith(mockFiles[1]);
});
});
Async Test Example:
import { render, screen, waitFor } from '@testing-library/react';
import { ProjectsPage } from '../pages/projects';
describe('ProjectsPage', () => {
it('loads and displays projects', async () => {
render(<ProjectsPage />);
await waitFor(() => {
expect(screen.getByText('My Projects')).toBeInTheDocument();
});
});
});
E2E Testing (Future)
Playwright Setup
cd apps/web
npm install -D @playwright/test
npx playwright install
Example E2E Test:
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:
# 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
@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)
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
- Test isolation: Each test should be independent
- Use fixtures: Don't repeat setup code
- Mock external services: Authentik, git operations
- Test edge cases: Empty lists, invalid inputs, errors
- Async properly: Use
pytest.mark.asyncioand async fixtures
Frontend
- Test behavior, not implementation: Check what user sees
- Use data-testid: For stable selectors
- Mock API calls: Don't hit real backend
- Test accessibility: Use
screen.getByRole - Snapshot sparingly: Only for complex UIs
Debugging Tests
Backend
# Run with debugger
pytest --pdb
# Run specific test with verbose
pytest -v -s test_file.py::test_name
# Show print statements
pytest -s
Frontend
# Debug mode
npm run test -- --reporter=verbose
# Show browser (for E2E)
npx playwright test --headed