Skip to main content
Browse the docs

Should You Write Tests When Vibe Coding?

Tests can be executable memory for Vibe Coding, but they can also consume context, slow regression, and freeze iteration. The real question is which evidence each risk deserves.

There are two common positions on testing in Vibe Coding.

One says that AI produces code too quickly to work safely without tests. Every new change can break something the current session does not understand. The other says that AI-generated tests consume the same context as production code, make regression slower, and turn a five-minute change into a long exercise in repairing assertions.

Both positions describe a real problem.

My conclusion is: Vibe Coding should include tests, but “add a test to every change and run the entire suite after every edit” is not a quality strategy.

Tests are not correctness for free. They are engineering assets with ongoing costs: someone—or an Agent—has to write, read, run, diagnose, and maintain them. The useful question is not “tests or no tests?” It is: what can this change break, and what evidence is worth buying for that risk?

Why tests matter even more in Vibe Coding

In traditional development, writing code is often the largest part of the cost. Vibe Coding changes that ratio. Generation becomes cheap; understanding and verification become the bottlenecks.

An Agent can change a dozen files in one task, then enter the next session without the reasoning that shaped the previous change. A specification can explain what the system should do. A test can reject a future implementation that violates that contract.

High-value tests provide four distinct benefits.

They preserve executable memory across sessions

A useful regression does not remember that “this function used to be written this way.” It remembers that “a slower, older request must not overwrite the user’s newer selection.”

After dozens of iterations, the original prompt, chat history, and developer memory may all be gone. That test still records a behavior the product cannot afford to lose.

They turn regression from intuition into evidence

“The page opened” does not prove that authorization, retries, out-of-order responses, transaction rollback, or job redelivery work correctly. Those failures rarely appear on the happy path a developer checks manually.

Tests can deliberately create dependency failures, concurrency, and boundary conditions. They replace “this looks fine” with evidence that another person or session can reproduce.

They expose weak design boundaries

Suppose testing one conflict branch requires rendering a full page, connecting to a real database, and waiting for a WebSocket event. The problem is not only that the test is difficult to write. It usually means business state, transport, and presentation have been coupled together.

Injectable repositories, explicit state machines, and replaceable clocks make testing easier because they also make runtime responsibilities clearer.

They stop an Agent from fixing only the visible symptom

Without a regression, an Agent can fix a bug by adding a special case for the current input. A later refactor removes the special case, along with the forgotten reason for it, and the bug returns.

A stronger sequence is to add the violated invariant to the owning specification, reproduce it with a failing regression, and then fix the implementation. The change now repairs a contract rather than one example.

How tests can damage Vibe Coding

“More tests are always better” ignores the two scarce resources in AI-assisted development: context and fast feedback.

Cost one: context waste

Test code is still code. If every source file has a larger test beside it, and every Agent session receives the complete regression log, the actual requirement, domain invariants, and call path can be pushed out of context.

The worst offenders are familiar:

  • tests that duplicate implementation structure line by line and fail after every refactor;
  • huge snapshots that produce hundreds of changed lines without identifying the changed behavior;
  • the same rule repeated at unit, component, and end-to-end levels;
  • test names such as works or should render, which force the reader to inspect every fixture before learning the intent.

These tests consume tokens while giving an Agent low-signal guidance.

Cost two: slower regression

Running everything after every change is reasonable when a project has a few dozen tests. It stops being reasonable when the system spans multiple applications, languages, and storage boundaries. The full suite changes from a feedback mechanism into a queue.

There is also a cost to interrupted attention. An Agent waits for the entire repository, then sees an unrelated package fail, and a local change becomes a detour into historical problems. The longer regression takes, the more likely developers are to run it less often and later, which reduces its practical value.

Cost three: freezing iteration too early

Exploratory interfaces, interactions, and product copy are meant to change quickly. If every DOM level, CSS class, and transitional label is locked into tests, the Agent is no longer protecting a product contract. It is preserving the archaeology of one implementation.

That creates the wrong incentive. To keep the suite green, an Agent patches the old structure instead of performing a cleaner refactor.

Cost four: false confidence

If the same Agent writes both implementation and tests from the same mistaken interpretation, both can be wrong in exactly the same way. Worse, when an existing test fails, the Agent may change the assertion until the new implementation turns green.

Tests cannot define correctness by themselves. The approved specification and observable user outcome define correctness; tests provide evidence. When behavior genuinely changes, update the contract first. Without a contract change, editing the assertion is not automatically a fix.

What qrp-platform shows in practice

qrp-platform is a useful case because it combines a Web application, Realtime gateway, Worker, Go Backend, and several domain packages. Its authentication, market data, backtesting, Agent, paper-trading, and live-trading paths carry very different levels of risk.

When I wrote this article, the latest recorded complete Web regression contained 152 suites and 981 tests. The repository-level runner also discovers test commands from its workspaces and runs several packages concurrently. That reveals both sides of the argument: regression coverage has become essential to continued change, while the full suite is far too broad for every local edit loop.

Several practices in the project are worth keeping:

  • Local iteration starts with tests focused down to the relevant files; a milestone runs the complete affected package.
  • The repository runner executes independent workspaces concurrently instead of serializing everything by default.
  • Test wrappers stay quiet on success and prioritize failed cases and diagnostic output on failure, rather than dumping the entire runner log back into Agent context.
  • Unit tests use injected fakes or mocks and do not depend on shared PostgreSQL or Redis. Real-infrastructure verification is separated from fast unit regression.
  • Each changelog entry connects requirements, implementation, and the actual verification commands, so a new session does not need to read the entire test tree to understand what a change must prove.

The project also contains a useful warning. Regression has sometimes been affected by concurrent load, QuickJS wall-clock limits, or failures in unrelated suites. The answer is not to rerun everything after every edit, and it is not to hide instability behind unlimited retries. Fast regression should remain deterministic; resource-sensitive and real-dependency checks belong in an explicit slower layer.

Decide by risk, not by file count

I start by placing a change into one of three risk bands.

RiskTypical changesDefault evidence
HighAuthorization and ownership, money or trading, persistence, migrations, concurrency, retries, idempotency, protocols, and data mappingAutomated regression is required, including failure paths and boundaries
MediumStore transitions, repository adapters, stable user interactions, and reusable domain calculationsA focused set of unit or contract tests, followed by the complete affected package at the milestone
LowExploratory layouts, temporary copy, visual-only adjustments, and disposable prototypesType/build checks plus targeted manual review; automate after the behavior stabilizes

One question makes the classification much easier: if an Agent accidentally breaks this three months from now, how quickly will we notice, and what will it cost?

Incorrect spacing is usually visible as soon as someone opens the page. A missing ownership filter may not be discovered until after data is exposed. The second change clearly deserves stronger automated evidence.

Bug fixes have a useful default rule: when a defect is reproducible and has future regression value, first add a test that fails for the violated contract. There is no need to create permanent test debt for a one-off script, an abandoned prototype, or a temporary environmental condition that cannot be observed deterministically.

Split feedback into four layers

One command should not be responsible for both edit-time feedback and release admission.

L0  Static feedback: formatting, schemas, types, compilation
L1  Focused regression: the current invariant and directly adjacent paths
L2  Component regression: the complete affected app or package
L3  Delivery regression: repository-wide checks, builds, required real integration, and critical user flows

The inner Vibe Coding loop runs L0 and L1. Once the local implementation is stable, run L2. Reserve L3 for a delivery milestone, release candidate, or a change that genuinely crosses several boundaries.

This does not lower the quality bar. It places evidence where it has the most value. If changing one variable always starts L3, people will eventually find ways around the gate. If L1 can reject the current mistake in seconds or another acceptably short interval, tests become part of iteration instead of an obstacle to it.

Protect behavior, not implementation

A good test should leave room for refactoring.

On the frontend, evidence can be divided into three layers:

  • Store and domain tests cover state transitions, out-of-order responses, optimistic updates, failure rollback, and cleanup on destroy.
  • View tests cover whether users can discover and perform an action, along with semantics, focus, keyboard paths, and state feedback.
  • A small number of browser flows prove that the major boundaries are actually connected. End-to-end tests should not enumerate an entire domain state machine.

On the backend, domain services, repository contracts, and real integrations can be tested separately. Unit tests inject clocks, random sources, queues, storage, and external clients. Repository tests cover serialization, namespaces, TTLs, missing records, dependency failures, and error mapping. Real infrastructure belongs in a separate integration layer with disposable data.

Do not assert how many times a private function was called unless the count is itself part of an idempotency, cost, or external side-effect contract. A domain test should not fail merely because a component changed from a div to a more appropriate element.

Deleting tests is not how you save context

The best way to reduce context usage is not to remove regression. It is to give the Agent a verification map so it can load only the evidence relevant to the current change.

A change should be traceable through this chain:

Requirement / invariant
  -> owning specification
  -> affected domain or package
  -> focused test files and commands
  -> complete affected regression
  -> required manual or integration acceptance

Keep tests close to their domain boundary, and name files and cases after the invariant they protect. Record the actual command, scope, and result in the changelog. A new session can then read the owning specification, relevant implementation, and a few focused cases instead of loading the whole test directory.

Runner output should also serve diagnosis. On success, report a concise scope and count. On failure, report the case name, smallest useful diff, original cause, and relevant file. Pages of successful logs provide almost no value to an Agent.

Slow tests need governance, not acceptance

Slow regression is not an unavoidable sign of a mature project. Give each feedback layer a time budget and keep track of the slowest cases.

  • Move real networks, databases, container startup, and model requests out of the unit layer.
  • Replace sleeps and timing luck with controlled clocks and deterministic random sources.
  • Run suites without shared state concurrently at app or package boundaries; serialize tests that intentionally share resources.
  • Cache reproducible build artifacts, but do not cache an unproven successful result.
  • Give flaky tests an owner, cause, and repair deadline. They may need to leave the fast gate temporarily, but unlimited retries must not disguise them as stable.

When a test is slow, ask what evidence it is purchasing. One real-browser acceptance covering a critical path may be justified. A hundred end-to-end tests repeating the same pure calculation are simply expensive duplication.

The test contract I give a Coding Agent

Instead of saying “remember to add tests,” I now give the Agent a more explicit contract:

1. Read the specification that owns this behavior and list the invariants this change must preserve.
2. Evaluate failure impact, discoverability, and change frequency, then choose the required evidence layer.
3. For a bug fix, reproduce the violated contract with a failing regression before fixing the implementation.
4. Assert observable behavior and failure semantics, not unrelated internal structure.
5. During iteration, run static checks and focused regression; once stable, run the affected package.
6. Reserve repository-wide, real-dependency, and complete browser regression for delivery gates that match the risk.
7. Record the actual command, scope, result, and anything not run. Do not replace evidence with “should pass.”

The point is not to make the Agent generate more code. It is to stop “a test was generated” from being mistaken for “the risk is controlled.”

Tests should protect change, not maximize coverage

The most valuable part of Vibe Coding is the ability to try ideas quickly. Tests should not destroy that speed, but speed is not a reason to ship unverified code to users.

Do not optimize for 100 percent coverage, and do not delete all tests to save tokens. A better question is: how much product risk does each minute of regression and each piece of Agent context actually protect?

A high-value test makes the next change faster because it quickly identifies a boundary that must not move. A low-value test makes every change slower because it only proves how the system happened to be implemented in the past.

Vibe Coding should include tests. Before adding each one, ask whether it preserves behavior the product cannot lose—or merely preserves the accidental shape of one generated implementation.

END / KEEP BUILDING