Chapter 9

Testing with AI

Testing is the part of programming most developers skip — and most regret skipping. AI removes the biggest excuse: it writes tests for you. This chapter teaches you how to make testing a natural, fast part of your workflow.


Why Developers Skip Testing

The reasons are always the same: it takes too long, it's boring, the code is simple enough, I'll add tests later. These reasons are understandable — and they're exactly the reasons AI eliminates. When AI can generate a complete test suite from your code in seconds, the calculus changes entirely.

The Real Cost

Untested code is a loan with compound interest. Every hour you save by skipping tests today costs you three hours of debugging tomorrow. Tests aren't overhead — they're insurance. And with AI, the premium is nearly free.


The Testing Pyramid

Not all tests are equal. The testing pyramid shows the ideal distribution of test types — more fast, focused unit tests at the base, fewer slow, broad tests at the top.

E2E Tests
Few — Slow, brittle, expensive
Integration Tests
Some — Test component interaction
Unit Tests
Many — Fast, focused, reliable

AI is especially powerful for unit tests and integration tests — the two categories that provide the most value per minute of effort. It can analyze a function's signature, logic branches, and edge cases, then generate comprehensive tests covering all of them.


Generating Unit Tests with AI

The most common and most valuable use of AI in testing: you give it a function, it gives you a complete test suite. Here's the prompt pattern that produces the best results.

Write unit tests for this function using Jest:

[paste your function]

Cover:
- Normal cases (expected inputs, expected outputs)
- Edge cases (empty arrays, null values, boundary values)
- Error cases (invalid inputs, missing parameters)

Use descriptive test names that explain what's being tested.
Group related tests with describe blocks.

Example: testing a filter function

function filterActivities(
  activities: Activity[],
  members: string[],
  day: string | null
): Activity[] {
  return activities.filter(a => {
    const memberMatch = members.length === 0 ||
      members.includes(a.member);
    const dayMatch = !day || a.day === day;
    return memberMatch && dayMatch;
  });
}

AI generates tests like:

describe('filterActivities', () => {
  const activities = [
    { id: '1', name: 'Soccer', member: 'Alice', day: 'Monday' },
    { id: '2', name: 'Piano', member: 'Bob', day: 'Tuesday' },
    { id: '3', name: 'Swim', member: 'Alice', day: 'Wednesday' },
    { id: '4', name: 'Dance', member: 'Carol', day: 'Monday' },
  ];

  describe('member filtering', () => {
    it('returns all activities when no members selected', () => {
      expect(filterActivities(activities, [], null)).toHaveLength(4);
    });

    it('filters by single member', () => {
      const result = filterActivities(activities, ['Alice'], null);
      expect(result).toHaveLength(2);
      expect(result.every(a => a.member === 'Alice')).toBe(true);
    });

    it('filters by multiple members', () => {
      const result = filterActivities(activities, ['Alice', 'Bob'], null);
      expect(result).toHaveLength(3);
    });
  });

  describe('day filtering', () => {
    it('returns all activities when day is null', () => {
      expect(filterActivities(activities, [], null)).toHaveLength(4);
    });

    it('filters by specific day', () => {
      const result = filterActivities(activities, [], 'Monday');
      expect(result).toHaveLength(2);
    });
  });

  describe('combined filtering', () => {
    it('applies both member and day filters', () => {
      const result = filterActivities(activities, ['Alice'], 'Monday');
      expect(result).toHaveLength(1);
      expect(result[0].name).toBe('Soccer');
    });
  });

  describe('edge cases', () => {
    it('handles empty activities array', () => {
      expect(filterActivities([], ['Alice'], null)).toEqual([]);
    });

    it('returns empty when no matches found', () => {
      expect(filterActivities(activities, ['Nobody'], null)).toEqual([]);
    });
  });
});

Notice the structure: descriptive describe blocks group related tests, each it block tests one specific behavior, and edge cases get their own group. This is production-quality test code generated in seconds.

PASS src/__tests__/filterActivities.test.ts
✓ returns all activities when no members selected (2ms)
✓ filters by single member (1ms)
✓ filters by multiple members
✓ returns all activities when day is null
✓ filters by specific day (1ms)
✓ applies both member and day filters
✓ handles empty activities array
✓ returns empty when no matches found
Tests: 8 passed, 8 total | Time: 0.34s

Integration Tests with AI

Integration tests verify that components work together correctly — not just in isolation. This is where you test that your React component renders properly, handles user interactions, and integrates with hooks and state.

Write integration tests for this React component
using React Testing Library:

[paste component code]

Test:
- Component renders without crashing
- Displays correct initial data
- User interactions work (clicking, typing, submitting)
- State updates reflect in the UI
- Error states display correctly

Use userEvent for interactions, not fireEvent.

The instruction to use userEvent instead of fireEvent matters — it simulates real user behavior more accurately. These small details in prompts produce significantly better test code.


Test-Driven Development with AI

Test-Driven Development (TDD) inverts the usual workflow: you write tests first, then write code to make them pass. AI makes TDD dramatically more accessible because it can generate comprehensive tests from just a specification — no existing code needed.

Red
Write failing test
Green
Write minimal code
Refactor
Clean up code
repeat

TDD with AI: the workflow

TDD Session: Building a Validation Function
You
I need a function validateActivity(data) that checks: name is required and at least 2 characters, day must be a valid weekday, time must be in HH:MM format. Write the tests first — don't write the implementation yet.
AI
[Generates 12 test cases covering: missing name, short name, valid name, invalid day, valid day, malformed time, valid time, multiple errors, etc.]
You
Good tests. Now write the minimum implementation that makes all tests pass.
AI
[Writes validateActivity function. All 12 tests pass.]
You
Now refactor the implementation for readability, keeping all tests passing.

Each step is explicit: tests first → implement → refactor. The tests serve as a specification, and the implementation is guaranteed correct because the tests define what "correct" means.

Why TDD + AI Works So Well

TDD's biggest barrier has always been the upfront cost of writing tests before you have code. AI eliminates this barrier — it generates comprehensive test suites from a plain-English specification in seconds. You describe what the code should do, AI writes the tests, then AI writes the code to pass them. The tests become your safety net for every future change.


What to Test (and What Not To)

Not everything needs tests. Knowing where to focus your testing effort is just as important as writing the tests themselves.

Worth Testing

  • Business logic (validation, calculations, filtering)
  • Data transformations (formatting, parsing, mapping)
  • State management (hooks, reducers, stores)
  • API contract compliance (request/response shapes)
  • Edge cases (empty data, null, boundary values)
  • User-facing interactions (forms, buttons, navigation)

Usually Not Worth Testing

  • Pure UI layout (CSS positioning, colors)
  • Third-party library internals
  • Simple pass-through components
  • Constants and configuration
  • Framework behavior (React re-renders)
  • Implementation details (internal state shape)

The principle: test behavior, not implementation. Your tests should verify what the code does, not how it does it internally. This makes tests resilient to refactoring — you can restructure the code completely, and as long as the behavior stays the same, the tests still pass.


AI for Edge Case Discovery

One of AI's most underappreciated testing abilities is finding edge cases you didn't think of. Human developers tend to test the "happy path" — the normal cases where everything works. AI excels at imagining the weird cases.

Here is my function:

[paste function]

What edge cases should I test that I probably haven't
thought of? Be creative — think about unusual inputs,
boundary values, timing issues, and failure modes.

AI typically identifies edge cases like: what if the array has exactly one element? What if two activities have the same time? What if the member name contains Unicode characters? What if the date is February 29th? These are the cases that cause bugs in production — and they're exactly the cases developers forget to test.

Pro Tip: The Adversarial Tester

Ask AI to act as an adversarial tester: "You're a QA engineer trying to break this function. What inputs would cause it to fail, return wrong results, or behave unexpectedly?" This framing produces much more thorough edge case coverage than a neutral "write tests" request.


Testing AI-Generated Code

Here's a critical meta-point: code generated by AI needs testing more than code you wrote yourself. You understand your own code's intent intuitively. AI-generated code might look correct but contain subtle logic errors, wrong assumptions, or hallucinated API calls.

🔁

Generate → Test → Iterate

After AI generates code, immediately ask it to write tests. Run the tests. Fix failures. This is the core loop.

🔍

Review Test Logic

AI-generated tests can have the same bugs as AI-generated code. Read the assertions — do they actually test the right thing?

Verify Expectations

Check that expected values in tests are actually correct. AI sometimes generates tests that pass but assert the wrong behavior.

🧪

Add Your Own Cases

AI covers patterns well but may miss domain-specific edge cases. Add tests for scenarios only you know about.

⚠️ Critical Warning

Tests that always pass are worthless. A common AI testing error is generating tests with assertions that are always true regardless of input (like testing that an array "is defined" instead of testing its contents). Always verify that your tests can actually fail — break the code intentionally and confirm the test catches it.


Adding Tests to Existing Code

Adding tests to an existing, untested codebase can feel overwhelming. AI helps by generating a test suite that covers the existing behavior — effectively creating a safety net before you make any changes.

Here is an existing function that has no tests:

[paste function]

Write a comprehensive test suite that documents its
current behavior. Include tests for:
- What the function does with typical inputs
- What it returns for edge cases
- How it handles errors

I'm about to refactor this function and need tests
to ensure I don't break anything.

This "characterization testing" approach captures existing behavior as tests before you change anything. Now you can refactor with confidence — if a test fails, you know exactly what behavior changed.


Common Mistakes

Testing Anti-Patterns

🧪 Practical Exercise

Take a function from one of your projects — ideally one with no tests. Practice three different testing approaches:

Run all tests. If they all pass on the first try, intentionally break the code and verify that tests catch the breakage.


Key Takeaways

Previous Chapter AI + Git Workflow
Next Chapter AI-First Development Methodology