Level 5Lesson 43⏱️ 90 min

Writing Tests with AI

AI writes the tests you never had time to write. Unit tests, edge cases, mocks, integration tests — in seconds.

Why AI + Testing is a Force Multiplier

Testing is the developer task most likely to be skipped under deadline pressure. AI reverses this — generating a solid test suite takes minutes, not hours. The bottleneck shifts from writing to reviewing.

What AI generates well:
  • Unit tests for pure functions (deterministic, no side effects)
  • Edge case discovery — null, empty, boundary, invalid input
  • Mock setup for external dependencies (DB, APIs, timers)
  • Integration test scaffolding
  • Test data builders and factory functions

Generating Unit Tests with /tests

// Select your function → /tests in Copilot Chat
// Or ask: "Write comprehensive unit tests for this function
//          using Jest + TypeScript"

// Your function:
function parseAmount(input: string): number {
  const cleaned = input.replace(/[$,s]/g, '')
  const parsed = parseFloat(cleaned)
  if (isNaN(parsed)) throw new Error('Invalid amount: ' + input)
  if (parsed < 0) throw new Error('Amount cannot be negative')
  return Math.round(parsed * 100) / 100
}

// AI generates:
describe('parseAmount', () => {
  it('parses a simple number string', () => {
    expect(parseAmount('42.50')).toBe(42.50)
  })
  it('strips dollar signs', () => {
    expect(parseAmount('$1,234.56')).toBe(1234.56)
  })
  it('strips commas and spaces', () => {
    expect(parseAmount('1 000.00')).toBe(1000.00)
  })
  it('rounds to 2 decimal places', () => {
    expect(parseAmount('10.999')).toBe(11.00)
  })
  it('throws for non-numeric input', () => {
    expect(() => parseAmount('abc')).toThrow('Invalid amount: abc')
  })
  it('throws for negative amounts', () => {
    expect(() => parseAmount('-5.00')).toThrow('Amount cannot be negative')
  })
  it('handles zero', () => {
    expect(parseAmount('0')).toBe(0)
  })
  it('throws for empty string', () => {
    expect(() => parseAmount('')).toThrow('Invalid amount')
  })
})

Edge Case Discovery Prompt

// Dedicated edge case prompt — gets you cases you'd never think of:

"List every edge case and boundary condition that should be
tested for this function. Be exhaustive — include:
- Empty/null/undefined inputs
- Boundary values (min, max, zero, negative)
- Type coercion surprises
- Unicode/special characters
- Very large/very small numbers
- Concurrency issues (if async)
- Error paths

Function: [paste your function]"

// Then follow up:
"Now write test cases for all of these using Vitest + TypeScript"

Mocking External Dependencies

// Ask: "Write Jest tests for this service, mocking the database
//        and the email service. Use jest.mock()"

export class UserService {
  constructor(
    private db: Database,
    private email: EmailService
  ) {}

  async registerUser(email: string, name: string): Promise<User> {
    const existing = await this.db.users.findByEmail(email)
    if (existing) throw new ConflictError('Email already registered')
    const user = await this.db.users.create({ email, name })
    await this.email.sendWelcome(user)
    return user
  }
}

// AI generates complete mock setup:
const mockDb = {
  users: {
    findByEmail: jest.fn(),
    create: jest.fn(),
  }
}
const mockEmail = { sendWelcome: jest.fn() }

describe('UserService.registerUser', () => {
  let service: UserService

  beforeEach(() => {
    jest.clearAllMocks()
    service = new UserService(mockDb as any, mockEmail as any)
  })

  it('creates a user and sends welcome email', async () => {
    mockDb.users.findByEmail.mockResolvedValue(null)
    mockDb.users.create.mockResolvedValue({ id: '1', email: 'a@b.com', name: 'Jay' })

    const result = await service.registerUser('a@b.com', 'Jay')

    expect(mockDb.users.create).toHaveBeenCalledWith({ email: 'a@b.com', name: 'Jay' })
    expect(mockEmail.sendWelcome).toHaveBeenCalledWith(result)
  })

  it('throws ConflictError if email already exists', async () => {
    mockDb.users.findByEmail.mockResolvedValue({ id: '2' })
    await expect(service.registerUser('a@b.com', 'Jay'))
      .rejects.toThrow(ConflictError)
    expect(mockDb.users.create).not.toHaveBeenCalled()
  })
})

TDD with AI: Test-First Workflow

1
Describe the function you want in plain English to AI
2
Ask: "Write the failing tests first — no implementation yet"
3
Run the tests — confirm they all fail (red)
4
Ask: "Now write the minimal implementation to pass these tests"
5
Run tests — confirm they pass (green), then ask: "Refactor this while keeping tests green"
// TDD prompt:
"I want a function called 'buildPaginationLinks' that:
- Takes currentPage, totalPages, and baseUrl
- Returns an object with prev/next/first/last URL strings
- Returns null for prev on first page, null for next on last page
- Throws if totalPages < 1

Write the tests first (Jest + TypeScript). No implementation."

Coverage Gap Analysis

// Use Claude Code to audit test coverage:
> Read the file src/services/payment.ts and the existing
  tests in tests/payment.test.ts
  What code paths are not covered by the current tests?
  List them and write tests for the top 5 gaps.

// Or with a coverage report:
> Here's my Jest coverage report: [paste]
  These functions have low coverage: [list]
  For each one, explain what cases are missing and
  write tests to bring coverage above 80%
Lesson 43 Quick Reference
/tests (Copilot)

Select a function, run /tests — generates full unit test suite

Edge case prompt

"List every edge case: null, boundary, Unicode, error paths" — then write tests

Mock setup

"Write tests mocking the DB and email service using jest.mock()"

TDD with AI

"Write failing tests first — no implementation" then "pass these tests minimally"

Coverage gaps

"Read the test file and the source — what code paths are missing?"

Test data builders

Ask: "Write a factory function that creates test User objects with sensible defaults"