# How to speed up tests for AI coding agents

> How to shorten an agent's feedback loop with focused tests, realistic boundaries, useful failures, and broader checks before shipping.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-19 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/fast-tests-for-agents/

A coding agent can edit a function quickly and then spend much longer waiting to learn whether the change works.

If that feedback takes too long, every correction becomes expensive. The agent makes a change, starts the checks, waits, reads an error, and starts again. A small misunderstanding can travel through several rounds of this.

Improving that loop is one of the first things I would consider before running more agents or changing models.

I want a check that answers the question currently blocking the work, with enough evidence to make the next decision. Sometimes that is a single test. Sometimes it requires the application and a real database. Running the appropriate check quickly helps the agent continue with less guessing.

## Useful feedback starts with a precise question

Suppose an agent is fixing a bug in a bookmark application. Archived bookmarks still appear in the ordinary search results.

We can express the expected behavior without discussing the implementation. An archived bookmark should be absent from ordinary search. It should remain available in the archive, and restoring it should bring it back to search.

Those expectations suggest a small group of checks. We do not need to exercise every settings screen to begin investigating this bug.

But a test that only checks whether a filtering function was called tells us much less. The function can be called with the wrong value. Another layer can put the archived bookmark back into the response.

I would start by locating the narrowest boundary where the incorrect behavior is observable. That could be the search query or the API response. The test should establish the bookmark's state and check the actual result.

## Keep cheap decisions close to the code

Some behavior can be checked without starting the application.

A function that decides whether a bookmark matches a filter can run against a few explicit inputs. Empty input, an archived record, and a normal record are easy to represent.

These checks are useful when the decision really lives in that function. They become less useful when the test recreates the application's logic instead of calling it.

For example, copying a filter expression into the test and asserting its output gives us two copies of the same idea. The application can change while the copied test keeps passing.

I would test the exported behavior directly. If reaching it requires constructing the whole application, I would inspect the dependencies before introducing another abstraction. Sometimes extracting a small decision helps. Sometimes a focused integration test is already the simpler answer.

The aim is an understandable failure. When the archive rule changes, I want the failing check to point at the rule rather than a long sequence of unrelated setup steps.

## Test the boundary that could be wrong

A unit test cannot tell us whether a database query applies a filter correctly if the database is replaced with a fake that already returns the expected records.

This is an easy mistake when asking an agent to add tests. It can build an elaborate mock, call the handler, and prove that the handler returns the mock's answer.

For the archived-bookmark bug, I would want at least one check using the real query against a test database. Create visible and archived records, execute the query, and inspect the returned identifiers.

That check can remain small. It does not need a browser or a real user account if neither is relevant to the query.

Other defects need different boundaries. A disabled button needs a browser. A permission check needs the server's actual authorization path. An export bug may require opening the generated file rather than checking whether an export function returned successfully.

Choosing the boundary well can remove a lot of unnecessary work without removing the evidence we need.

## Give the agent a focused command

A repository should make its test entry points easy to discover.

If the only documented command starts every service and runs every check, that is what an agent is likely to use. It may also guess a command that does less than its name suggests.

I would document how to run one relevant file, how to check a larger area, and which checks are required before finishing. Those commands can live beside the development instructions in `AGENTS.md` or the README.

Test runners already support selection. [Vitest's filtering guide](https://vitest.dev/guide/filtering) explains filtering by filename and test name. It also points out that a name filter alone can still require loading test files, so including the file can avoid unnecessary discovery work.

There is no need to invent a custom test orchestration service for a small repository. A working command and a short explanation are enough.

I would ask the agent to report what it selected. “Tests passed” is ambiguous when it means one file from a much larger suite.

## Focused checks and release checks serve different moments

During the repair, I want fast feedback on the behavior being changed.

Before accepting it, I want to check the surrounding assumptions too. The archived-bookmark fix can affect archive counts, restore behavior, and pagination. A narrow test that proves the original bug is fixed does not cover those automatically.

This suggests a gradual expansion. Reproduce the bug, correct it, run the relevant area, then complete the project's required checks.

The final set depends on the change. Updating a label needs different evidence from replacing the search query. A small text edit does not justify creating a large new test suite. A change to record visibility deserves more scrutiny.

I would let the agent explain this selection in terms of risks. “Search and archive share the query, so I checked both” helps me assess the work. A list of commands without their scope makes that harder.

When another change lands before integration, the relevant checks need to describe the combined code. An earlier passing run does not automatically cover later edits.

## Work through one repair from failure to acceptance

Let's make the bookmark example more specific. Suppose ordinary search shows one matching bookmark per page. We have three matching records in the test database, ordered from newest to oldest:

| Bookmark | State |
| --- | --- |
| CSS layout notes | Archived |
| CSS grid guide | Visible |
| CSS selectors reference | Visible |

The first focused check asks the real search query for its first page. The expected result is the grid guide. A broken query returns the archived layout notes, reproducing the reported bug.

An agent could repair that by removing archived records from the response after fetching the first page. Now the archived item disappears, but the page is empty. The visible bookmarks are still in the database, beyond the slice the query fetched.

The acceptance case needs to say what the user should receive. With a page size of one, ordinary search should return the grid guide first, then the selectors reference. If the interface displays a total, it should count the two visible matches. Those expectations expose the incomplete repair without needing a browser to visit every page in the application.

During the edit, I would keep rerunning this focused group against the query. Once it passes, I would widen the checks to the archive query and restoration. The archived layout notes should remain accessible in the archive. Restoring it should return it to ordinary search in the position implied by the application's existing sort rule.

Then I would use the browser flow that connects these operations. Archive a result, search again, open the archive, and restore it. This checks whether the interface sends the intended request and displays the changed results. A passing query test cannot answer those interface questions.

Finally, run the project's required checks against the completed change. The report should distinguish the focused query tests from the browser flow and the broader checks. If the browser flow failed, a passing query test remains useful evidence, but the repair is unfinished.

This example uses a deliberately small dataset so we can explain every expected result. The useful speed improvement comes from catching the wrong repair early, before repeatedly running the rest of the application around it.

## Measure the waiting before optimizing it

A slow test run can spend its time in places you would not expect.

The tests themselves can be quick while every run rebuilds the application. Setup may download dependencies, or a failed network request may wait for a timeout before the useful assertion starts.

I would inspect the runner's timing output and separate setup from execution. Then I would change the slowest unnecessary step and measure again under comparable conditions.

Cold and warm runs deserve separate attention. A cache can make a repeated local run feel fast while every clean CI job pays the full setup cost. That does not make caching wrong. It means the result needs a description of the conditions.

Without measurements from the actual project, I would avoid promises about how much faster agents become. Reducing test latency helps a repeated feedback loop, but the overall task can still be limited by review or unclear requirements.

## Parallel tests need separate state

Running checks concurrently can help when they are independent and the machine has capacity.

It can also expose hidden dependencies. Two tests use the same account. One deletes a bookmark while another expects it to exist. Each passes alone, and together they fail intermittently.

For browser tests, [Playwright provides isolated browser contexts](https://playwright.dev/docs/browser-contexts), giving tests separate cookies and browser storage. The application's backend state still needs attention. Separate browser sessions can edit the same database row.

I would use unique test records or isolated databases where appropriate. Each test should create the state it needs instead of depending on a previous test to leave it behind.

This helps agents too. A test that only passes in a particular order is difficult to reproduce from a single failure. It encourages repeated retries when the useful response is to repair the shared-state assumption.

More workers will not rescue a suite whose tests continually interfere with each other.

## Sharding helps after the suite can run independently

A larger suite can be split across machines. This is usually called sharding.

[Playwright's configuration](https://playwright.dev/docs/api/class-testconfig#test-config-shard) supports running a selected shard of the suite. The practical benefit depends on how the work is divided and how much setup each job repeats.

If one group contains a long end-to-end flow and the others finish quickly, the whole run still waits for that group. If every job spends most of its time booting the same expensive environment, adding jobs can multiply setup cost.

I would look at the distribution before increasing the shard count. Keeping independent tests reasonably balanced can help. So can avoiding an unnecessarily broad setup dependency.

For a small application, this can wait. A single machine with a focused test command may be entirely adequate. I would add distributed execution when a measured bottleneck justifies maintaining it.

## Browser tests should act on what the user sees

A browser test becomes fragile when it depends on the accidental shape of the HTML.

If a test finds the archive button through a long chain of CSS classes, a harmless layout change can break it. The agent then spends time repairing the test even though the user flow still works.

[Playwright's best-practice guide](https://playwright.dev/docs/best-practices) recommends user-visible behavior and locators based on attributes such as roles and accessible names. That gives the test a closer relationship to what someone can do in the interface.

I would open the bookmark list, find the archive control by its accessible name, use it, and check the resulting list. An assertion that a component exists skips the action we care about.

Headless execution is convenient for repeatable checks. It still leaves some questions for direct use, particularly when a change affects visual layout, keyboard interaction, or the feel of an editing workflow. I would match the verification to the feature rather than assume one mode settles everything.

## Make failures explain what happened

An agent works better with a failure it can investigate.

“Expected true, received false” forces it to reconstruct the scenario. A failure naming the missing bookmark and the search response gives it a place to start.

For browser failures, retain the useful evidence. A trace or screenshot can show whether the control was missing, a request failed, or the application reached a different page. Logs should describe the failure without exposing private test credentials.

I would also distinguish a test failure from an environment failure. A database that never started tells us little about archive behavior. The runner should make that difference visible.

If a check is flaky, repeated passes do not explain the earlier failure. Investigate it or label the uncertainty clearly. Quietly increasing retries makes the green result harder to interpret.

## Keep the expected behavior independent

An agent can write an implementation and a test that agree on the wrong behavior.

For example, it decides archived bookmarks should be permanently deleted, then writes a test proving they disappear. Both changes are internally consistent. They violate the requirement to restore a bookmark later.

I want the expected behavior settled before inspecting the implementation. A short description of the user's action and result gives the review an independent reference.

For a bug fix, I would ask whether the regression test fails on the old behavior and passes after the repair. That is stronger evidence than a test added after the change which has never caught the defect.

For a low-risk change, I would also ask whether a new test earns its maintenance cost. Existing checks and a direct verification can be enough. Generating tests is cheap; carrying irrelevant tests through every future change still creates work.

## Find the slowest useful check

I would take the last change where an agent spent a lot of time waiting and look at the actual sequence.

Which check was it waiting for? What did that check tell it? Could a smaller check have answered the immediate question? Did the final verification still cover the surrounding behavior?

Then I would improve that one path. Document the focused command, remove unnecessary setup, and make the failure easier to understand. The free [Testing JavaScript Applications course](https://flaviocopes.com/courses/testing/) covers the underlying choices across functions, databases, browsers, and CI.

Once the path works, the agent can use it on the next relevant change. I would measure that workflow before deciding whether the project needs more test machinery.
