Skip to content
Let's Talk
Testing

AI-Driven Testing Strategies That Actually Work

Felix Schmidt

There is a growing narrative that AI will write all your tests for you. The reality is more nuanced. AI tools like Claude Code can dramatically improve your testing workflow, but only if you use them strategically. Blindly generating tests produces brittle, low-value test suites that give you false confidence.

This post covers the strategies that have actually worked for me — generating meaningful test cases, AI-assisted TDD, finding edge cases, and maintaining test suites over time. All examples use TypeScript and Jest.

Generating Test Cases from Code

The most straightforward use case: point AI at a function and ask it to generate tests. But the difference between useful and useless output depends entirely on how you prompt.

Bad prompt: "Write tests for this function."

Good prompt: "Write tests for this function. Cover the happy path, null/undefined inputs, empty arrays, boundary values, and any error conditions. Each test should have a descriptive name that explains the expected behavior."

Here is a concrete example. Given this function:

export function paginate<T>(items: T[], page: number, pageSize: number): {
  data: T[];
  totalPages: number;
  currentPage: number;
  hasNext: boolean;
  hasPrev: boolean;
} {
  const totalPages = Math.ceil(items.length / pageSize);
  const start = (page - 1) * pageSize;
  const data = items.slice(start, start + pageSize);

  return {
    data,
    totalPages,
    currentPage: page,
    hasNext: page < totalPages,
    hasPrev: page > 1,
  };
}

AI generates tests that a human might not immediately think of:

describe('paginate', () => {
  const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  it('returns the correct slice for the first page', () => {
    const result = paginate(items, 1, 3);
    expect(result.data).toEqual([1, 2, 3]);
    expect(result.currentPage).toBe(1);
    expect(result.hasPrev).toBe(false);
    expect(result.hasNext).toBe(true);
  });

  it('returns the last partial page correctly', () => {
    const result = paginate(items, 4, 3);
    expect(result.data).toEqual([10]);
    expect(result.hasNext).toBe(false);
  });

  it('returns empty data for a page beyond the total', () => {
    const result = paginate(items, 100, 3);
    expect(result.data).toEqual([]);
    expect(result.hasNext).toBe(false);
  });

  it('handles an empty array', () => {
    const result = paginate([], 1, 10);
    expect(result.data).toEqual([]);
    expect(result.totalPages).toBe(0);
    expect(result.hasNext).toBe(false);
    expect(result.hasPrev).toBe(false);
  });

  it('handles pageSize larger than the array', () => {
    const result = paginate(items, 1, 100);
    expect(result.data).toEqual(items);
    expect(result.totalPages).toBe(1);
    expect(result.hasNext).toBe(false);
  });

  it('handles pageSize of 1', () => {
    const result = paginate(items, 5, 1);
    expect(result.data).toEqual([5]);
    expect(result.totalPages).toBe(10);
    expect(result.hasNext).toBe(true);
    expect(result.hasPrev).toBe(true);
  });
});

Notice the edge cases: empty array, page beyond total, pageSize of 1, pageSize larger than the dataset. These are the tests that catch real bugs.

AI-Assisted TDD

This is where AI shines most. Instead of generating tests after the code, use AI to generate tests before the implementation.

The workflow:

  1. Describe the function''s behavior in natural language
  2. Ask AI to write the test suite
  3. Review and adjust the tests — this forces you to think about the specification
  4. Implement the code to make the tests pass
Prompt: "I need a function called parseTimeRange that takes a string
like '2h30m', '45m', '1h', or '90s' and returns the total number of
seconds. Write Jest tests for this function covering valid inputs,
invalid formats, zero values, and edge cases."

AI produces a comprehensive test suite. You review it, realize you forgot to specify whether negative values are allowed, add that to the spec, and then implement. The tests become the specification — exactly how TDD is supposed to work, but faster.

Finding Edge Cases Humans Miss

Humans think in happy paths. We write the test for the input we expect, verify it works, and move on. AI thinks differently — it systematically considers categories of inputs:

  • Boundary values: 0, -1, MAX_SAFE_INTEGER, empty strings, single-character strings
  • Type coercion traps: "0" vs 0, null vs undefined, NaN
  • Unicode and encoding: Emoji in string inputs, multi-byte characters, RTL text
  • Concurrency: What happens when two calls race? Is the function idempotent?
  • State dependencies: Does the function behave differently on the second call?

I regularly ask Claude Code: "What edge cases am I missing in this test file?" It usually finds two or three I had not considered.

Property-Based Testing with AI

Property-based testing is underused because writing generators and properties is harder than writing example-based tests. AI lowers that barrier significantly.

import fc from 'fast-check';

describe('paginate properties', () => {
  it('data length never exceeds pageSize', () => {
    fc.assert(
      fc.property(
        fc.array(fc.integer()),
        fc.integer({ min: 1, max: 1000 }),
        fc.integer({ min: 1, max: 100 }),
        (items, page, pageSize) => {
          const result = paginate(items, page, pageSize);
          expect(result.data.length).toBeLessThanOrEqual(pageSize);
        }
      )
    );
  });

  it('totalPages is consistent with items length and pageSize', () => {
    fc.assert(
      fc.property(
        fc.array(fc.integer()),
        fc.integer({ min: 1, max: 100 }),
        (items, pageSize) => {
          const result = paginate(items, 1, pageSize);
          expect(result.totalPages).toBe(Math.ceil(items.length / pageSize));
        }
      )
    );
  });
});

AI-generated property tests are surprisingly good. The key insight is that properties describe invariants — things that must always be true regardless of input. AI is effective at identifying these because it can reason about the function''s contract.

Test Maintenance and Refactoring

Tests rot. When the implementation changes, tests break — not because of bugs, but because the test was coupled to implementation details. AI helps here in two ways:

1. Identifying brittle tests: Ask AI to review your test file and flag tests that depend on implementation details rather than behavior. Tests that mock internal methods, assert on call counts, or check intermediate state are candidates for refactoring.

2. Updating tests during refactoring: When you change a function''s signature or behavior, AI can update the corresponding tests to match. This is faster than manual updating and less error-prone.

Integration vs. Unit Testing

AI is excellent at unit tests because the scope is small and the contract is clear. Integration tests are harder because they require understanding of system boundaries, external dependencies, and environment configuration.

My recommendation: use AI for unit tests aggressively. For integration tests, use AI to generate the structure — the describe blocks, the setup/teardown, the test names — and fill in the assertions manually. The human judgment about what constitutes a meaningful integration test is still essential.

Honest Limitations

AI-generated tests have real limitations:

  • They test what the code does, not what it should do: If the implementation has a bug, AI might write tests that verify the buggy behavior. Always review generated tests against the specification, not just the implementation.
  • Coverage theater: AI can hit 100% code coverage with tests that assert nothing meaningful. Coverage is necessary but not sufficient.
  • Flaky test generation: AI sometimes generates tests with timing dependencies, hard-coded dates, or order-dependent assertions. Review for determinism.
  • Over-mocking: AI tends to mock aggressively. If every dependency is mocked, you are testing the mocks, not the code.

Practical Strategy Summary

  1. Use AI for TDD: Write tests first, implement second. Let AI generate the initial test suite from a natural language spec.
  2. Always review generated tests: Treat AI-generated tests like AI-generated code — useful starting point, not a finished product.
  3. Ask for edge cases explicitly: "What edge cases am I missing?" is the highest-value prompt for testing.
  4. Use property-based testing for utilities: AI makes writing properties and generators much faster.
  5. Separate unit and integration strategies: AI is great for unit tests, supportive for integration tests.
  6. Run mutation testing: The ultimate check. If your tests pass when the code is mutated, they are not testing the right things. Tools like Stryker work well here.

Testing is where AI provides the clearest, most measurable value. Not because it writes perfect tests, but because it writes the tests you would have skipped — the edge cases, the boundary conditions, the error paths. Combined with human review, that is a testing strategy that actually works.

This topic relevant to your team? Let's discuss how I can help.

This website uses third-party services (Google reCAPTCHA, Calendly) that may set cookies. See our Privacy Policy for details.