How to Write Tests with an AI Assistant

Writing tests with an AI assistant removes most of the friction that causes developers to skip or defer testing. Tests are repetitive, edge-case enumeration is tedious, and the boilerplate for mocks and fixtures looks nearly identical across hundreds of test files. AI handles all of this well. The challenge is knowing how to direct it effectively — because vague prompts produce shallow tests, and unchecked AI-generated tests can create false confidence by asserting things that are always true regardless of the implementation.

What AI Does Well in Test Writing

AI coding assistants are genuinely strong at the parts of test writing that consume the most time without requiring domain expertise:

  • Boilerplate and setup — test fixtures, mock configuration, setUp/tearDown patterns. Give the AI an existing test file and ask it to follow the same structure for a new function, and it matches the style consistently.
  • Edge case enumeration — ask “what edge cases should I test for this function?” and the AI systematically lists boundary conditions, empty inputs, null handling, integer overflow, invalid types, and error paths that developers miss when tired or rushing.
  • Parameterised tests — converting a table of inputs and expected outputs into a parameterised test suite is purely mechanical. AI produces these reliably and saves significant time on data-heavy test coverage.
  • Property-based test properties — identifying invariants like “output is always non-negative,” “the function is idempotent,” or “sorting a sorted list is a no-op” and generating Hypothesis or fast-check tests around them.
  • Test naming — descriptive names like test_create_user_raises_value_error_when_email_missing come naturally to AI and are a small but real friction point to write manually.

What AI handles less well: tests where the correct expected output requires domain knowledge not present in the code. For a function that calculates a tax rebate using specific business rules, the AI cannot know the correct expected value — you have to supply it. The AI writes the test structure; you supply the domain-correct assertions.

Two Approaches: Test-After vs Test-First

Test-after means writing the code first, then asking the AI to generate tests for it. This is faster to start and the AI can read the implementation directly. The risk is that the tests verify what the code currently does rather than what it should do — if the implementation has a subtle bug, the AI may encode that bug into the expected values.

Test-first (AI-assisted TDD) means giving the AI a specification and asking it to write failing tests before any implementation exists. You then implement the code to pass those tests. This produces higher-quality tests because they express the intended behaviour independently of any implementation. For functions with complex logic or important edge cases, test-first is worth the extra upfront investment.

In practice: use test-after for routine additions to an existing codebase where speed matters. Use test-first when building something new where getting the specification right is the hard part.

Figure 1 — Test-first vs test-after: key differences

Dimension Test-First (TDD) Test-After Test qualityHigh — spec-drivenMedium — impl-driven SpeedSlower upfrontFaster to start Bug riskLower — catches impl bugsHigher — may encode bugs Best forNew features, complex logicAdding to existing code

Writing Effective Prompts for Test Generation

The quality of AI-generated tests rises sharply with better prompts. Vague requests produce shallow coverage; specific requests with context produce tests you would actually write yourself.

A strong prompt for a new function: “Write pytest unit tests for this function. It should [specification]. Test the happy path, boundary conditions (empty list, maximum values, zero), and error cases (invalid type, missing required field). Follow the fixture pattern in the existing test file I’ve included.” A strong prompt for edge case discovery: “Here is my function and its current tests. What edge cases am I missing? List them, then write the test code for each.”

Including existing tests in the prompt — either pasted or via a file reference — is especially valuable. The AI matches the testing style, fixture naming, assertion patterns, and import structure of your existing suite rather than inventing its own conventions. This is the fastest way to get tests that fit seamlessly into a codebase without manual reformatting.

Handling Mocks and External Dependencies

Tests for code that calls external services — databases, APIs, email providers, payment processors — require careful mock setup. AI generates reasonable mocking code but you should verify three things before accepting it:

  • Patch location — the mock should be applied where the name is looked up in the module under test, not in the library itself. A common AI mistake is patching requests.get globally instead of mymodule.requests.get.
  • Return value shape — a mock for a database query that returns ORM objects should return ORM objects (or realistic fakes), not a boolean or a string. Check that the mock’s return value matches the actual interface.
  • Error paths — if the dependency can raise exceptions, the tests should cover how the code handles them. AI often generates happy-path mocks and omits error-path coverage; explicitly request both.

For projects with clear dependency injection patterns — passing services as constructor arguments or function parameters — AI-generated tests tend to be more reliable because the mock injection point is explicit. For code reaching into global singletons or module-level imports, expect to adjust the generated mocking code more often.

Reviewing AI-Generated Tests

Before committing AI-generated tests, review them for three specific failure modes that are easy to miss on a quick read:

Tests that always pass. A test that makes no meaningful assertion — assert result is not None when a more specific check is possible, or assert True as a placeholder — provides no value and creates false confidence. Look for assertions that would pass even if the function returned the wrong value.

Incorrect expected values. The AI infers expected values from the implementation. If the implementation has a bug, the AI may confidently assert the wrong answer. For business-critical calculations — financial amounts, security checks, data transformations with specific rules — verify expected values independently rather than trusting the AI’s inference.

Test interdependence. Tests should not depend on each other’s execution order or share mutable state. AI-generated tests sometimes accumulate state across test methods accidentally — a list that gets appended to in one test and read in another. Run the tests in random order at least once to catch these dependencies.

Incrementally Improving Coverage with AI

One of the most practical applications of AI test writing is methodical coverage improvement on an existing codebase with patchy tests. Run your coverage tool, identify the functions or branches with the lowest coverage, and work through them systematically — paste each function into the AI with context about its purpose, ask for tests covering the uncovered paths, review and commit.

This workflow is tedious to do manually and well-suited to AI assistance. The AI can see from the coverage report and the code exactly which paths need testing and generates targeted tests efficiently. A focused session of two or three hours using this pattern can meaningfully move coverage numbers on a real codebase, and the tests produced are generally as good as hand-written tests for the same functions — sometimes better, because the AI is more systematic about boundary conditions than a developer working quickly.

Property-Based Testing

AI assistants handle property-based testing particularly well. Ask the AI to convert example-based tests into Hypothesis tests (Python), fast-check tests (JavaScript), or the equivalent for your stack, and it identifies reasonable invariants and generates the strategy definitions competently. Common properties it generates reliably: output is always within a specified range, the function is idempotent, round-tripping through encode/decode produces the original value, sorting is stable. Property-based tests catch bugs that example-based tests miss — off-by-one errors, overflow conditions, unexpected interactions between fields — and writing good property definitions by hand requires careful thinking that AI genuinely accelerates.

Using AI for Test Maintenance

Tests break when the code they test changes. AI is useful here too: paste a failing test alongside the updated function and ask “this test is failing after I changed the function — update the test to match the new interface while preserving its coverage intent.” This is faster than manual test updates for large refactors and ensures the updated test still covers what it was meant to cover rather than just being updated to pass. Be deliberate about the distinction: some failing tests should prompt you to revert the code change, not update the test. Ask the AI to flag whether the test failure suggests the change broke something it should not have, rather than just generating a passing test automatically.

AI-assisted test writing works best as a collaboration: the AI generates structure, boilerplate, and edge cases; you supply domain knowledge about correct expected values, verify that mocks match reality, and review for tests that look valid but provide no genuine coverage. The combination produces test suites faster and more completely than either approach alone, and significantly reduces the friction that causes testing to be deferred in the first place.

Leave a Comment