Test-Driven Development
TDD with AI: Writing Specs That Generate Tests
AI tools excel at generating tests from precise specifications — combine human spec-writing with AI test generation.
AI + TDD: The Powerful Combination
AI coding tools are excellent at generating test code — when given the right specifications. The combination of human-written specifications and AI-generated tests is more powerful than either alone.
Without specs: AI generates tests that cover the happy path but miss edge cases, error conditions, and security-relevant scenarios.
With specs: AI generates comprehensive tests because the specifications tell it exactly what behavior to verify.
How to Prompt for Test Generation
Effective prompts include:
- The function signature or component interface
- The expected behavior for each scenario
- Edge cases and error conditions
- The testing framework to use
The Spec-to-Test Pipeline
- Write requirement in clear format
- Translate to test case descriptions
- AI generates test code from descriptions
- Human reviews: Are assertions meaningful? Are edge cases covered?
- Implement code to pass the tests
Reviewing AI-Generated Tests
AI tests need human review. A checklist:
- Does it test behavior, not implementation?
- Are assertions meaningful (not just "doesn't throw")?
- Are error paths tested, not just happy paths?
- Are edge cases covered (empty input, boundary values, null)?
- If a bug existed, would this test catch it?
What AI Misses (Add These Manually)
- Security edge cases: script injection, SQL injection
- Race conditions
- Business-specific domain rules
- Authorization failures
- Error propagation when dependencies fail
Key Takeaways
- AI generates tests best when given precise specifications — vague prompts produce vague tests
- The pipeline: write requirement → define test cases → AI generates test code → human reviews
- Review checklist: behavior not implementation, meaningful assertions, error paths, edge cases
- AI generates excellent happy-path tests; humans must add edge cases, security checks, and domain-specific scenarios
Example
// Effective prompt → quality AI-generated tests
// Prompt: "Write Vitest tests for validateEmail(email: string): boolean
// Rules: must have @, domain must have dot, no spaces, max 254 chars"
describe('validateEmail', () => {
it('accepts valid email addresses', () => {
expect(validateEmail('user@example.com')).toBe(true);
expect(validateEmail('user.name+tag@sub.domain.com')).toBe(true);
});
it('rejects emails without @', () => {
expect(validateEmail('userexample.com')).toBe(false);
});
it('rejects emails with domain missing dot', () => {
expect(validateEmail('user@example')).toBe(false);
});
it('rejects emails with spaces', () => {
expect(validateEmail('user @example.com')).toBe(false);
expect(validateEmail('user@ example.com')).toBe(false);
});
it('rejects emails exceeding 254 characters', () => {
const longEmail = 'a'.repeat(250) + '@b.com';
expect(validateEmail(longEmail)).toBe(false);
});
it('rejects empty string and null-like inputs', () => {
expect(validateEmail('')).toBe(false);
});
});// Effective prompt → quality AI-generated tests
// Prompt: "Write Vitest tests for validateEmail(email: string): boolean
// Rules: must have @, domain must have dot, no spaces, max 254 chars"
describe('validateEmail', () => {
it('accepts valid email addresses', () => {
expect(validateEmail('user@example.com')).toBe(true);
expect(validateEmail('user.name+tag@sub.domain.com')).toBe(true);
});
it('rejects emails without @', () => {
expect(validateEmail('userexample.com')).toBe(false);
});
it('rejects emails with domain missing dot', () => {
expect(validateEmail('user@example')).toBe(false);
});
it('rejects emails with spaces', () => {
expect(validateEmail('user @example.com')).toBe(false);
expect(validateEmail('user@ example.com')).toBe(false);
});
it('rejects emails exceeding 254 characters', () => {
const longEmail = 'a'.repeat(250) + '@b.com';
expect(validateEmail(longEmail)).toBe(false);
});
it('rejects empty string and null-like inputs', () => {
expect(validateEmail('')).toBe(false);
});
});