Skip to main content
DoneThat

AI Adoption GuideSoftwareTest

Unit Test Generation

LLM generates unit tests with edge cases directly from function signatures and source code, using tools like CodiumAI.

Software processDiscoverDesignBuildTestReleaseAdoptSupportRetire

By Don, DoneThat’s AI coach · updated

What unit test generation does

Unit test generation uses a large language model to draft tests from the code under test. The usual inputs are a function signature, the function body, and any nearby types or helpers the model can see in the same file or module. The output is a candidate test suite: arrange steps, act calls, and assertions that exercise happy paths and a set of edge cases the model can infer from the implementation.

Tools in this category (CodiumAI is a common example) sit in the editor or CI surface and propose tests next to the source. The model does not replace the engineer’s ownership of the suite. It drafts. The engineer still decides which cases belong, which assertions are meaningful, and whether the new tests land in the repository.

When the function source is missing, generation should produce empty output. A signature alone is not enough to invent behavior-faithful tests without guessing. Empty output is the correct failure mode: it forces the engineer to supply the body (or a richer context pack) before any test file is written.

When this outcome fits

This practice fits teams that already write unit tests but want faster coverage of new functions, refactors, and small pure modules. It pays off most when:

  • The code under test is mostly pure or has clear seams (injected dependencies, narrow interfaces).
  • Edge cases are numerous but mechanical (nulls, empty collections, boundary integers, error returns).
  • Review bandwidth exists: someone will read every generated assertion before merge.
  • The suite already has conventions the model can mirror (naming, fixture style, assertion library).

It is a poor fit when tests must encode product policy the source does not state, when the function is a thin wrapper over unmocked I/O, or when the team treats green tests as proof without reading them. Generated suites that assert implementation trivia (private call order, incidental string formatting) create brittle noise. Prefer generation for behavior at the public surface of a unit.

Speed is the outcome here: wall-clock time from “function written” to “first credible unit tests in review.” Quality still depends on human judgment. Pair this with practices that stress inputs the happy-path suite never sees, such as Adversarial Input Generator, when you need hostile or malformed cases beyond what a first pass usually drafts.

How the workflow runs

A practical loop looks like this:

  1. Select the unit. Point the tool at a function or small module. Include the full source. If the body is unavailable, stop; emit nothing.
  2. Provide context the model cannot invent. Types, error enums, and invariants that live outside the file should be pasted or linked. Without them, the model fills gaps with plausible fiction.
  3. Generate a draft suite. Ask for happy path plus edge cases derived from branches, guards, and return types visible in the source. Prefer one focused test per behavior over a single mega-test.
  4. Run the draft locally. Failures are information: wrong assumption, missing mock, or a real bug. Do not mass-accept failing tests to “make CI green.”
  5. Edit for intent. Rename tests to match team language. Drop assertions that only mirror the current implementation. Add cases the model skipped (concurrency, locale, clock) if they matter.
  6. Land with review. Treat the PR like any other test change: diff-readable, convention-aligned, and owned by the author who merges it.

Keep prompts operational, not theatrical. Example shape: “Given this function body and these types, draft unit tests for the public behavior. Include empty input, null where the type allows it, and each early-return branch. Use our existing test runner and assertion style. If the function body is absent, return no tests.”

Human-in-the-loop is non-negotiable at two gates: before any file is written to disk as “done,” and before merge. The model accelerates drafting; the engineer remains accountable for false confidence.

Edge cases and failure modes to expect

Models are good at enumerating obvious boundaries once they can see the branches. They are weaker at domain rules that never appear in code comments or types. Watch for these failure modes:

  • Hallucinated APIs. The draft calls helpers or mock methods that do not exist. Catch this by compiling or running the suite immediately.
  • Tautological assertions. The test reconstructs the same expression as the implementation and “proves” nothing about external behavior. Prefer assertions on observable results and error kinds.
  • Snapshot of bugs. If the source is wrong, generated tests will lock the wrong behavior in. Generation is not a substitute for specification review.
  • Missing source → invented body. Some tools will still emit tests from a signature. Reject that path. Configure or prompt for empty output when the function source is missing so you never land fiction.
  • Flaky dependencies. Tests that hit time, randomness, or shared temp state without control will flake later. Stabilize them in review, or route flake diagnosis to Flaky Test Root Cause Analysis when instability shows up in CI.
  • Over-coverage theater. Hundreds of low-value cases inflate coverage metrics without reducing risk. Keep the suite tied to behaviors you would defend in a design review.

When edge-case generation is the goal, ask explicitly for cases implied by each conditional and each validated input. Then subtract anything that duplicates another test. Density without redundancy beats volume.

Review checklist before you merge

Use a short gate so speed does not become silent debt:

  • Does every test name state a behavior a teammate would recognize?
  • Would a correct rewrite of the function still pass these tests, or do they pin incidental structure?
  • Are mocks limited to true boundaries (network, disk, clock), not every collaborator?
  • Did you run the suite with the production-like config your CI uses?
  • Is there at least one failure-path test when the source can fail?
  • Did you delete drafts that only restate the happy path in different words?

If the unit is part of a larger user journey, keep unit tests narrow and push cross-service flows to higher layers. For end-to-end exploration driven by an agent, see Autonomous E2E Test Agent. Unit generation and E2E agents solve different layers; mixing them in one PR usually confuses ownership and flake triage.

Document team conventions in a short snippet the tool can read (runner, factory helpers, forbidden patterns). Consistency cuts edit time more than clever prompts.

Measuring speed without fooling yourself

Track leading indicators that match the outcome:

  • Time from merge-ready function to first reviewed unit tests (median, not only best case).
  • Share of generated lines that survive review unchanged versus rewritten or deleted.
  • Escape defects that unit tests should have caught in that module (qualitative count in retros is enough; avoid vanity coverage deltas as the sole score).

Ignore raw “tests generated per day.” That metric rewards spam. Prefer “behaviors newly protected per engineer-hour” as a planning heuristic, even if you only estimate it roughly in sprint notes.

Roll out in one repo or package first. Calibrate empty-output behavior on signature-only inputs so the team trusts the tool’s refusal mode. Expand once review load is predictable and engineers stop treating drafts as merge-ready by default.

Unit test generation earns its place when it shortens the path from working code to a reviewed, behavior-focused suite, and when empty output on missing source keeps fiction out of the tree. The model drafts; the engineer still lands the tests.

Is this worth automating for you?

Whether this pays back depends on how much time it takes your team today. Most teams estimate that from memory, and the estimate is usually wrong in one direction or the other.

DoneThat reconstructs where the time actually went, with no timers to forget, so you can measure the baseline before committing to a project and check the gain afterward.

Measure the baseline first