Files
headquarter/docs/development/testing.md
T
Fusion 83f94b1f09 docs: comprehensive documentation overhaul
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
2026-05-19 14:18:20 +02:00

366 lines
7.8 KiB
Markdown

# 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(<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**:
```typescript
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
```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/)