Test Patterns
Best practices and patterns for writing tests in the Heimdall project.
Rust Unit Tests
Basic Pattern
// At the bottom of any .rs file
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_function() {
let result = my_function("input");
assert_eq!(result, "expected");
}
#[tokio::test]
async fn test_async_function() {
let result = async_function().await;
assert!(result.is_ok());
}
#[test]
fn test_error_handling() {
let result = function_that_can_fail("bad input");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("expected error"));
}
}
Integration Tests
// tests/my_feature_test.rs
use axum::http::StatusCode;
use heimdall_api::{create_test_app, TestClient};
#[tokio::test]
async fn test_health_endpoint() {
let app = create_test_app().await;
let client = TestClient::new(app);
let response = client.get("/health").send().await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_protected_endpoint_requires_auth() {
let app = create_test_app().await;
let client = TestClient::new(app);
let response = client.get("/v1/me").send().await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
TypeScript Unit Tests (Vitest)
Basic Pattern
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { myFunction } from './utils';
describe('myFunction', () => {
it('handles valid input', () => {
expect(myFunction('input')).toBe('expected');
});
it('handles edge cases', () => {
expect(myFunction('')).toBe('');
expect(myFunction(null)).toBeNull();
});
});
API Route Tests
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST } from './route';
import { NextRequest } from 'next/server';
vi.mock('@/lib/auth', () => ({
getServerSession: vi.fn(),
}));
describe('GET /api/user', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns 401 when not authenticated', async () => {
const { getServerSession } = await import('@/lib/auth');
vi.mocked(getServerSession).mockResolvedValue(null);
const request = new NextRequest('http://localhost/api/user');
const response = await GET(request);
expect(response.status).toBe(401);
});
it('returns user data when authenticated', async () => {
const { getServerSession } = await import('@/lib/auth');
vi.mocked(getServerSession).mockResolvedValue({
user: { id: '123', name: 'Test' },
});
const request = new NextRequest('http://localhost/api/user');
const response = await GET(request);
expect(response.status).toBe(200);
});
});
React Component Tests
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('calls onClick when clicked', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
fireEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
E2E Tests (Playwright)
Basic Pattern
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test('completes user flow', async ({ page }) => {
await page.goto('/path');
await expect(page.getByRole('heading')).toHaveText('Expected');
await page.click('button');
await expect(page).toHaveURL('/new-path');
});
});
Authentication Flow
test.describe('Authentication', () => {
test('redirects unauthenticated users to login', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveURL(/\/login/);
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'invalid@example.com');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await expect(page.getByText(/invalid credentials/i)).toBeVisible();
});
});
Navigation Tests
test.describe('Navigation', () => {
test('navigates through main menu', async ({ page }) => {
await page.goto('/');
await page.click('a[href="/about"]');
await expect(page).toHaveURL('/about');
});
test('mobile navigation works', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await page.click('[data-testid="mobile-menu-button"]');
await expect(page.getByRole('navigation')).toBeVisible();
});
});
Best Practices
General
- Write tests first when fixing bugs (TDD for bug fixes)
- Test edge cases - empty inputs, nulls, boundaries
- Test error paths - not just happy paths
- Keep tests isolated - no shared state between tests
- Use descriptive names - test name should explain what's tested
Naming Conventions
| Language | Unit Tests | Integration | E2E |
|---|---|---|---|
| Rust | Inline #[cfg(test)] or tests/*_test.rs | tests/*.rs | N/A |
| TypeScript | *.test.ts | *.integration.test.ts | e2e/*.spec.ts |
Mocking
Rust:
- Use
mockallcrate for mocking traits - Create test fixtures in
tests/fixtures/ - Use
wiremockfor HTTP mocking
TypeScript:
- Use
vi.mock()for module mocking - Use
mswfor API mocking - Create test utilities in
tests/utils/