| name | sea-test-generator |
| description | Generate comprehensive tests from SEA specifications. Creates unit tests, integration tests, and E2E tests with proper coverage. |
SEA Test Generation
Generate comprehensive test suites from SEA specifications including unit tests, integration tests, and E2E tests with proper mocking, fixtures, and coverage.
When to Use
Invoke when:
- Handlers have been generated
- New API endpoints added
- Domain logic implemented
- Test coverage is low
Usage
User invokes with:
- "Generate tests for {context}"
- "Create tests from spec"
- "Generate integration tests"
Claude invokes automatically:
- After handler generation
- When test coverage is low
- Before PR completion
Test Generation Process
Step 1: Parse Spec for Test Scenarios
SCENARIOS=$(yq e '.test_scenarios // []' "$SPEC_FILE")
ENDPOINTS=$(yq e '.api_surface.endpoints[]' "$SPEC_FILE")
SCHEMAS=$(yq e '.api_surface.schemas[]' "$SPEC_FILE")
RULES=$(yq e '.domain.rules // []' "$SPEC_FILE")
Step 2: Generate Unit Tests
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { CreateUserHandler } from '../../src/gen/handlers/create-user';
import type { Context } from '@sprime01/sea';
describe('CreateUserHandler', () => {
let handler: CreateUserHandler;
let mockContext: Context;
let mockDomain: any;
beforeEach(() => {
mockDomain = {
create: vi.fn(),
findById: vi.fn(),
update: vi.fn(),
delete: vi.fn()
};
mockContext = {
domain: mockDomain,
logger: {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn()
},
events: {
publish: vi.fn()
}
} as unknown as Context;
handler = new CreateUserHandler(mockContext, mockContext.logger);
});
describe('handle', () => {
const validRequest = {
body: {
name: 'John Doe',
email: 'john@example.com',
preferences: { theme: 'dark' }
}
};
it('should create user with valid request', async () => {
const expectedUser = {
id: '123',
name: 'John Doe',
email: 'john@example.com',
preferences: { theme: 'dark' },
createdAt: new Date()
};
mockDomain.create.mockResolvedValue(expectedUser);
const response = await handler.handle(validRequest);
expect(response.status).toBe(200);
expect(response.body).toEqual(expectedUser);
expect(mockDomain.create).toHaveBeenCalledWith(validRequest.body);
expect(mockContext.events.publish).toHaveBeenCalledWith({
type: 'UserCreated',
payload: expectedUser
});
});
it('should reject invalid email', async () => {
const invalidRequest = {
body: {
name: 'John Doe',
email: 'not-an-email',
preferences: {}
}
};
const response = await handler.handle(invalidRequest);
expect(response.status).toBe(400);
expect(response.body.error).toBe('Validation error');
expect(mockDomain.create).not.toHaveBeenCalled();
});
it('should reject missing name', async () => {
const invalidRequest = {
body: {
email: 'john@example.com'
}
};
const response = await handler.handle(invalidRequest);
expect(response.status).toBe(400);
expect(response.body.error).toBe('Validation error');
});
it('should handle domain errors', async () => {
mockDomain.create.mockRejectedValue(new Error('Duplicate email'));
const response = await handler.handle(validRequest);
expect(response.status).toBe(500);
expect(response.body.error).toBe('Internal server error');
expect(mockContext.logger.error).toHaveBeenCalledWith(
'Handler error',
expect.any(Object)
);
});
it('should sanitize preferences', async () => {
const requestWithScript = {
body: {
name: 'John Doe',
email: 'john@example.com',
preferences: {
theme: 'dark',
script: '<script>alert("xss")</script>'
}
}
};
const sanitizedUser = {
id: '123',
name: 'John Doe',
email: 'john@example.com',
preferences: { theme: 'dark' },
createdAt: new Date()
};
mockDomain.create.mockResolvedValue(sanitizedUser);
const response = await handler.handle(requestWithScript);
expect(response.status).toBe(200);
expect(response.body.preferences).not.toHaveProperty('script');
});
});
});
Step 3: Generate Integration Tests
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { setupTestApp } from '../helpers/test-setup';
import type { TestContext } from '../helpers/types';
describe('User API Integration Tests', () => {
let container: PostgreSqlContainer;
let context: TestContext;
beforeAll(async () => {
container = await PostgreSqlContainer.start();
const connectionString = container.getConnectionUri();
context = await setupTestApp({
database: { connectionString }
});
await context.migrate.up();
});
afterAll(async () => {
await context.app.close();
await container.stop();
});
describe('POST /api/users', () => {
it('should create user and persist to database', async () => {
const response = await context.request
.post('/api/users')
.send({
name: 'Jane Doe',
email: 'jane@example.com'
});
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Jane Doe');
expect(response.body.email).toBe('jane@example.com');
const dbUser = await context.db.query(
'SELECT * FROM users WHERE id = $1',
[response.body.id]
);
expect(dbUser.rows[0]).toMatchObject({
name: 'Jane Doe',
email: 'jane@example.com'
});
});
it('should reject duplicate email', async () => {
const user = {
name: 'John Doe',
email: 'john@example.com'
};
await context.request.post('/api/users').send(user);
const response = await context.request.post('/api/users').send(user);
expect(response.status).toBe(409);
expect(response.body.error).toContain('email');
});
it('should publish UserCreated event', async () => {
const eventsPromise = context.waitForEvent('UserCreated');
await context.request.post('/api/users').send({
name: 'Event User',
email: 'event@example.com'
});
const event = await eventsPromise;
expect(event).toMatchObject({
type: 'UserCreated',
payload: {
name: 'Event User',
email: 'event@example.com'
}
});
});
});
});
Step 4: Generate E2E Tests
import { test, expect } from '@playwright/test';
test.describe('User Management E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.fill('[name="email"]', 'admin@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
test('complete user creation workflow', async ({ page }) => {
await page.click('a[href="/users"]');
await expect(page).toHaveURL('/users');
await page.click('button:has-text("Create User")');
await expect(page.locator('h1')).toContainText('Create User');
await page.fill('[name="name"]', 'E2E Test User');
await page.fill('[name="email"]', 'e2e@example.com');
await page.selectOption('[name="role"]', 'user');
await page.click('button:has-text("Create")');
await expect(page.locator('.alert-success')).toContainText(
'User created successfully'
);
await expect(page.locator('table')).toContainText('e2e@example.com');
await page.click('text=e2e@example.com');
await expect(page).toHaveURL(/\/users\/[a-z0-9-]+/);
await expect(page.locator('h1')).toContainText('E2E Test User');
await expect(page.locator('[data-testid="user-email"]')).toContainText(
'e2e@example.com'
);
});
test('validation prevents invalid user creation', async ({ page }) => {
await page.goto('/users/create');
await page.click('button:has-text("Create")');
await expect(page.locator('[data-testid="name-error"]')).toContainText(
'Name is required'
);
await expect(page.locator('[data-testid="email-error"]')).toContainText(
'Email is required'
);
await page.fill('[name="name"]', 'Test User');
await page.fill('[name="email"]', 'not-an-email');
await page.click('button:has-text("Create")');
await expect(page.locator('[data-testid="email-error"]')).toContainText(
'Invalid email format'
);
});
test('user search and filter', async ({ page }) => {
await page.goto('/users');
await page.fill('[name="search"]', 'admin');
await page.press('[name="search"]', 'Enter');
await expect(page.locator('table tbody tr')).toHaveCount(1);
await expect(page.locator('table')).toContainText('admin@example.com');
await page.fill('[name="search"]', '');
await page.press('[name="search"]', 'Enter');
await expect(page.locator('table tbody tr')).toHaveCountGreaterThan(1);
});
});
Step 5: Generate Test Fixtures
import { faker } from '@faker-js/faker';
export interface UserFixture {
id: string;
name: string;
email: string;
preferences: Record<string, unknown>;
createdAt: Date;
}
export function createUserFixture(overrides?: Partial<UserFixture>): UserFixture {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
preferences: {
theme: faker.helpers.arrayElement(['light', 'dark', 'auto']),
notifications: faker.datatype.boolean()
},
createdAt: faker.date.past(),
...overrides
};
}
export function createUserFixtures(count: number): UserFixture[] {
return Array.from({ length: count }, () => createUserFixture());
}
export const validUserFixture = () => createUserFixture({
name: 'Valid User',
email: 'valid@example.com'
});
export const invalidEmailFixture = () => createUserFixture({
email: 'not-an-email'
});
export const duplicateEmailFixture = () => createUserFixture({
email: 'duplicate@example.com'
});
Step 6: Generate Test Helpers
import { Express } from 'express';
import request from 'supertest';
import { setupApp } from '../../src/app';
import type { Database } from '../../src/infrastructure/database';
export interface TestContext {
app: Express;
request: request.SuperTest<request.Test>;
db: Database;
migrate: {
up: () => Promise<void>;
down: () => Promise<void>;
};
waitForEvent: (eventType: string) => Promise<any>;
}
export async function setupTestApp(options?: {
database?: { connectionString: string };
}): Promise<TestContext> {
const app = await setupApp(options);
return {
app,
request: request(app),
db: app.get('database'),
migrate: {
up: async () => {
await app.get('database').migrate.up();
},
down: async () => {
await app.get('database').migrate.down();
}
},
waitForEvent: (eventType: string) => {
return new Promise((resolve) => {
const eventBus = app.get('eventBus');
eventBus.once(eventType, resolve);
});
}
};
}
Test Coverage Targets
| Type | Minimum Coverage |
|---|
| Unit Tests | 80% |
| Integration Tests | 60% |
| E2E Tests | Critical paths only |
Test Organization
tests/
├── unit/
│ ├── handlers/ # Handler unit tests
│ ├── domain/ # Domain logic tests
│ └── infrastructure/ # Infrastructure tests
├── integration/
│ ├── api/ # API integration tests
│ └── database/ # Database integration tests
├── e2e/
│ ├── workflows/ # E2E workflow tests
│ └── scenarios/ # User scenario tests
├── fixtures/ # Test fixtures
└── helpers/ # Test helpers
Integration
- Integrates with
sea-handler-gen for test generation after handlers
- Integrates with Vitest for unit/integration tests
- Integrates with Playwright for E2E tests
- Integrates with Testcontainers for database tests
Output
After test generation:
## Tests Generated
**Context**: {context}
**Spec**: spec/context.yaml
### Test Files Created
- Unit tests: {count}
- Integration tests: {count}
- E2E tests: {count}
- Fixtures: {count}
- Helpers: {count}
### Coverage Estimate
- Unit: {estimated}%
- Integration: {estimated}%
- E2E: {critical paths}
### Next Steps
1. Review generated tests
2. Run tests: pnpm test
3. Check coverage: pnpm test:coverage
4. Add missing assertions
5. Implement missing domain logic