Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Unit tests are not worse than useless. They are a poor substitute for everything else, though, and most of the damage people attribute to them comes from how they are written and what they are asked to prove. The useful question is not whether to write unit tests, but which kind of test proves which kind of behavior.

Where the claim comes from and what the rebuttal argues

The phrase “unit tests are worse than useless” is the premise of a DEV Community article by Abass Ajanaku, whose title ends with “- Not quite.” Ajanaku rejects the binary choice between unit tests and end-to-end tests. His position is that a team should combine test types according to what each one needs to prove. The article is available at https://dev.to/fxola/unit-tests-are-worse-than-useless-not-quite-55jd. The page shows a September 27 date, but the year is not reliably visible in the retrieved copy, so treat its exact publication year as unconfirmed.

The rebuttal is a qualified one. It does not argue that unit tests are always valuable, and it does not offer a measured ratio of unit to end-to-end tests. It makes a practitioner’s case that each layer of testing has a job, and that dropping one layer in favor of another creates blind spots.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What unit tests do well

Ajanaku’s case for unit tests rests on two practical advantages.

  • Speed. Unit tests run quickly, which makes them practical to run on every change and gives developers feedback while the code is still in their head.
  • Edge-case reach. Small, fast tests make it affordable to exercise many boundary conditions in core logic, such as empty inputs, rejected values, and unusual combinations. Many of these cases would be cumbersome to reach through a full application run.

Neither advantage depends on unit tests being the only kind of test. Both are reasons to keep them for the logic that has many branches.

Where unit tests go wrong

The strongest criticism in the material is not that unit tests are slow or narrow. It is that they often assert the wrong thing. Dan Abramov, a React contributor, describes this problem in an interview with Software Engineering Unlocked, published under the title “Inside Facebook: Bad Tests Are Worse Than Product Issues.” The episode is at https://www.software-engineering-unlocked.com/episode-13-bad-tests-dan-abramov/. The interview does not state a date in the retrieved material.

Abramov’s account is that tests coupled to internal module details did not describe user-visible behavior. When React was substantially rewritten, those tests became unhelpful: they failed because the internals had changed, not because anything a user would notice had broken. The team’s response was to rewrite tests against public behavior. Abramov explains the principle in this exchange:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“And so now we know that. Whatever the solution we have, if we change it to some other equivalent solution that also solves this problem, that task will still pass.”

This is a single practitioner’s account of one project. It is useful because it names a specific failure mode, but it is not a controlled study and should not be read as a general finding about all codebases.

A test strategy that combines test types

The alternative Ajanaku proposes is a mix. Each test type covers a different risk.

End-to-end tests for critical user flows

End-to-end tests can focus on the mission-critical flows that matter most to users, such as signing up, checking out, or completing a core task. They reflect what a user actually experiences, so they give the strongest evidence that the product works as a whole. Their cost is speed and infrastructure. Ajanaku notes that relying only on slower end-to-end tests can increase infrastructure cost, which is why he suggests keeping their number well below the number of fast unit tests. That is his recommendation for his own examples, not a ratio established by measurement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Contract and integration tests at boundaries

Contract or integration tests check the connections between core behavior and the infrastructure around it, such as a database adapter, an external API client, or a message queue. A unit test can prove that business logic behaves correctly against a stand-in. A contract test proves that the real adapter honors the same promises as that stand-in. Without this layer, a unit suite can pass while the production wiring is broken.

Rank #3
Sale

Separating core logic from infrastructure: a worked example

Ajanaku’s design example starts with core business logic that depends directly on a database. Every test of that logic therefore needs a database, which makes unit tests slow and fragile. His fix is dependency inversion.

  1. Define a repository contract, meaning an interface that describes what the core needs from storage, such as saving and loading an order.
  2. Make the core use case depend on that contract rather than on the database client.
  3. Write an in-memory implementation of the contract and use it in fast unit tests of the core logic.
  4. Write the production adapter so that it implements the same contract against the real database.
  5. Write contract tests that run the same expectations against each adapter, so the in-memory version and the production version are held to one standard.

Ajanaku describes the shared-contract step in these words: “This process of ensuring they depend on a shared contract is known as dependency inversion.” The article is explicit about the cost. The design adds code and an abstraction layer, and a team has to decide whether that overhead is worth it for the system at hand. For a small script or a thin CRUD service, it may not be.

The following sketch illustrates the pattern. It is an illustration written for this article, not code from the cited sources.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
// Contract the core depends on
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | undefined>;
}

// In-memory adapter used by fast unit tests
class InMemoryOrderRepository implements OrderRepository {
  private orders = new Map<string, Order>();
  async save(order: Order) { this.orders.set(order.id, order); }
  async findById(id: string) { return this.orders.get(id); }
}

The production database adapter implements the same interface, and a shared contract test suite runs against both.

Comparing test types

The table below summarizes how the cited material characterizes each test type. Where the material does not address a property, the cell says so.

Property Unit tests (core logic, in-memory stand-ins) Contract or integration tests End-to-end tests (critical user flows)
Feedback speed Fast, per Ajanaku Not stated in the article Slower, per Ajanaku
Running cost Low, since fast execution is the advantage cited Not stated in the article Higher; relying only on these may increase infrastructure cost, per Ajanaku
Behavioral fidelity Depends on whether the test asserts public behavior, per Abramov Checks that real adapters honor the contract Reflects user-facing flows most directly
Edge-case reach High; small tests make many core cases practical Limited to boundary behavior Hard to reach many edge cases
Boundary confidence Low on its own, since infrastructure is replaced High for the boundary it covers Covers boundaries as part of whole flows
Refactoring friction High if tests encode internal structure, per Abramov Not stated in the article Not stated in the article

The table is a qualitative comparison drawn from the practitioner accounts above. It is not a benchmark, and none of the cited material measures these properties.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Checklist for deciding whether a unit test earns its place

The behavior-first framing in Abramov’s account gives a usable test for any individual test:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • What behavior or requirement does this test protect?
  • If the implementation changes but the behavior stays the same, should this test keep passing?
  • When this test fails, does the failure point to a regression a user or the business would care about?
  • Does the test depend on internal method names, call order, or private state? If so, consider asserting the observable result instead.
  • Would an end-to-end test or a contract test prove this more directly, and at what cost?

A test that fails these questions is not useless in principle, but it is probably pinned to structure and will cost more to maintain than it returns.

What the evidence does and does not establish

The material is practitioner reasoning, not measurement. Ajanaku’s article and Abramov’s interview describe design choices and one project’s experience. They do not report defect rates, maintenance costs, or productivity figures, and no such statistic should be attached to either.

A 2010 Slashdot discussion titled “Whatever Happened To Programming?” (https://developers.slashdot.org/story/10/03/07/0043215/whatever-happened-to-programming, dated March 7, 2010) contains a commenter, phantomfive, who wrote: “Unit tests are a tool, a good tool, but only one tool.” That is one commenter’s opinion, not a conclusion reached by the site or by a study. The same commenter suggested that test counts can grow exponentially in complex systems. No measurement supports that claim, so it should be treated as an impression rather than a fact.

The practical consequence is that no fixed test pyramid or ratio is proven by these sources. The strongest defensible position is the one both practitioners reach by different routes: keep unit tests for logic that has many cases, keep end-to-end tests for the few flows that matter most, check boundaries with contract tests, and judge every test by the behavior it protects.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Because the rebuttal is about design, the most useful next step is usually a review of existing tests rather than a rewrite. Ask which tests fail when internals change but users see nothing, and which important flows have no test at all.

Bottom line for teams

Unit tests are worth keeping where they are fast, cover many logic cases, and assert behavior that matters. They are worth less when they mirror implementation details or stand in for end-to-end and boundary checks. The claim that they are “worse than useless” overstates the problem. The better reading is that unit tests answer one question well, and teams should not ask them to answer the others.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.