Turning one function into a test plan a reviewer will sign off
Most test generators hand back a describe block with three assertions passing no matter what the function does. An assertion checking a result is defined, on a function returning a boolean, is a green tick and nothing else. It survives a rewrite of the whole body. The bench above reads the body first, then writes cases pointed at what the body actually does: the guards, the numbers your comparisons are pinned to, the branch with no early return, the call reaching outside the function. Every row in the plan names the line of code behind it. Every row the parser could not settle says so in plain words.
What the analyzer reads before writing a single test
Four passes run over the pasted code, and each one feeds a different part of the plan.
- The signature. Parameter names, type annotations where the language has them, and default values. Where annotations are missing, the type comes from how the body treats the parameter: a comparison against a number types it as a number, a call to trim or split types it as a string, a call to map or a length check types it as a list.
- Guard clauses. A condition whose body returns early or raises gets both a case and an expected value, read straight out of the guard. This is the one place the tool knows what should happen, because you wrote it down.
- Literal comparisons. Every comparison against a hard-coded number becomes a cut-off. Both sides of the cut-off get a case.
- Branches with no early return. A condition changing a variable and falling through gets a case on each side too, marked as yours to complete, since nothing in the code says what the result should be.
- Calls leaving the function. Network calls, the system clock, random values, database access and file writes are flagged. With stubbing on, the file arrives with the setup block already written.
Where each category of case comes from
| Category | Read from | The defect it catches |
|---|---|---|
| Happy path | Parameter types, with values chosen to clear every guard | A signature change nobody propagated |
| Type guard | A typeof, is_numeric, isinstance or instanceof check | A caller passing the string "12" where a number was meant |
| Null input | A null, None or falsy check with an early exit | An optional field arriving absent from an upstream service |
| Empty input | A length, count, empty or isEmpty check | The zero-row result set nobody tried in staging |
| Boundary | A comparison against a literal number | The off-by-one at the edge of a range |
| Equivalence class | An equality check against a literal string | A plan, role or status name spelled differently in one place |
| Branch | A condition with no early return | A path exercised by no test at all |
| Negative | Parameters with no guard covering them | The contract you never wrote down |
The last row is the one worth arguing about in review. A negative case appears when nothing in the function rejects a wrong type or a missing argument for a given parameter. The tool has no opinion on whether the answer should be a thrown error, a coerced value or a fallback. What it does is put the question in front of you, in a row with a name, before the choice gets made by accident in production.
Both sides of every cut-off
Off-by-one defects live at the edge of a range, so a single case in the middle of a range finds nothing. When the body compares a parameter against a literal number, the plan gets a pair: one value crossing the line, one value sitting on the other side of it.
if (minutesToFirstReply < 0) {throw new RangeError('...');}
if (priority >= 3) {target = target / 2;}-1 throws RangeError (guard says so)0 returns a boolean (other side)3 halved target (you fill in)2 full target (other side)Read the difference between those two pairs. The first pair has a known expected value on the failing side, because the guard throws and names the error class. The second pair has none, because the branch changes a local variable and falls through to a shared return. The generated file marks the second pair with a comment saying why, and leaves a loose assertion until you replace it. Reviewers should treat a file still full of loose assertions as unfinished work.
Three depths sit above the editor. Smoke keeps the happy path and the guards with a known answer, which is the set worth running on every commit. Standard adds the other side of each branch. Adversarial adds wrong types, missing arguments and a ten thousand character string for every parameter no guard protects, which is the set to run before a release rather than on every save.
The plan is the deliverable, the file is a draft
Two exports come off the same analysis, and they go to different places.
- Copy plan puts a markdown table on your clipboard. Paste it into the pull request, the ticket, or the review comment where the test approach gets agreed. A reviewer reads eight rows in twenty seconds. Nobody reads eighty lines of test code that carefully.
- Plan CSV downloads the same rows for a test management tool, a spreadsheet of manual checks, or a traceability matrix where someone has to sign off that each case has a home.
- Copy file and Download give you the runnable skeleton, named for the framework you picked.
Sending the plan out before the code gets written is the part QA teams get value from. Agreeing the cases with the developer costs one comment. Discovering after merge that nobody tested the empty list costs a release.
Test names end up in the failure report
A test name is read at its least convenient moment: red text in a pipeline log at the end of the day. The three styles produce the same cases with different names.
| Style | Produces | Fits |
|---|---|---|
| should | should return null when minutesToFirstReply is not a number | Jest and Mocha suites, where the describe block already names the unit |
| given, when, then | given minutesToFirstReply is not a number, when firstReplySlaBreach runs, then it returns null | Teams mapping tests back to acceptance criteria |
| plain | returns null when minutesToFirstReply is not a number | pytest and JUnit, where the method name is already long |
Whichever you pick, the condition half of the name comes from the code. A failing test called "returns null when minutesToFirstReply is not a number" points at a line. A failing test called "test2" starts a bisect.
Branch reach is not code coverage
The branch reach figure counts conditions in the pasted function with at least one case aimed at them, divided by the conditions found. That number comes from reading the source text, not from running anything. It says the plan has not skipped a branch. It does not say your assertions are meaningful, that the lines inside each branch ran, or anything at all about the rest of the file.
Real coverage comes from an instrumented run: a Jest or Vitest run with the coverage flag, PHPUnit with Xdebug or PCOV, JUnit with JaCoCo, pytest with coverage. Treat the number here as a checklist tick during planning, then measure properly once the tests run. The test coverage analyzer works on reports from those runs.
Where the generated expectations are wrong
Five failures are worth knowing before a generated file goes near a repository.
- Loose assertions pass by accident. Any case where the expected value could not be read carries an assertion checking the result exists. A function returning the wrong number still passes. These are placeholders and each one carries a comment saying so.
- Parsing is regex based, not a real syntax tree. A nested closure, a deeply chained ternary, or a guard spread over several statements gets read imperfectly. The plan is a starting set, and reading it against the function is part of the job.
- Values are generic. A string parameter gets a plain word, a list gets three integers. Domain formats such as an email address, an ISO timestamp or a currency code need real values, and the tool has no way to know which parameter is which.
- Stubs are named after common libraries. The setup block assumes a repository, a clock, or a global fetch. Point the stubs at your own collaborators before running the file.
- Async support stops at JavaScript. Jest, Vitest and Mocha files await the call and assert on a rejected promise. A Python coroutine gets a direct call, so add the await and the marker your runner needs.
A pass through one function
- Paste the function with its signature line, and pick the framework matching the language. A fragment of a body with no signature parses as nothing.
- Read the plan before the file. Check the parameter types the analyzer inferred, shown under the editor, since a mistyped parameter sends every case in the wrong direction.
- Copy the plan into the ticket or the pull request and agree the cases with whoever wrote the function. Rows marked as yours to decide are the conversation.
- Switch to Adversarial and look only at the new rows. Each one is a parameter with no guard behind it. Some deserve a test, some deserve a guard in the function instead.
- Download the file, replace every loose assertion with the value you agreed, and swap the generic inputs for values from your domain.
- Run it with coverage on, then compare what the runner reports against the branch reach shown here. A gap between the two is a branch the plan reached and the assertions did not.
What this generator leaves out
- No cross-function analysis. One function at a time. A call into another function in your codebase is opaque, which is also why the tool never guesses what the return value should be.
- No property based testing. No generated input ranges, no shrinking. For a pure function with wide input, a library such as fast-check or Hypothesis finds cases no fixed table will.
- No mutation testing. Whether your assertions would survive a changed operator is a question for Stryker, Infection or mutmut, and it is the honest measure of whether the tests do anything.
- No integration or contract tests. Databases, HTTP boundaries and message queues need integration tests and API tests instead.
- No test runner configuration. No config file, no fixtures directory, no CI step. The output is one file for a project already set up to run tests.
- No class-level scaffolding beyond the constructor. A PHPUnit or JUnit file instantiates the class with no arguments. A constructor taking dependencies needs those wired by hand.
Nothing you paste leaves your browser. Parsing, case planning and code generation all run inside this page, so a function holding business rules or credentials stays on your machine. Load the page once, drop your network connection, and the bench above keeps working.
