Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteiTechGuides 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.
What unit tests do well
Ajanaku’s case for unit tests rests on two practical advantages.
#1 Best Overall
- 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:
“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.
Recommended Free Tools
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
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.
- Define a repository contract, meaning an interface that describes what the core needs from storage, such as saving and loading an order.
- Make the core use case depend on that contract rather than on the database client.
- Write an in-memory implementation of the contract and use it in fast unit tests of the core logic.
- Write the production adapter so that it implements the same contract against the real database.
- 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.
// 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.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:
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstall- 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.
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.
Quick Recap
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.

