I was walking someone through our testing approach a few years back, and they mentioned something that gave me pause. They said they’d heard of companies dropping unit tests entirely, doing all their verification through end-to-end browser automation. Testing everything through Cypress, or Selenium, or whatever their tool was, top to bottom, all the time.

I told them: That’s a mistake.

Not because E2E tests are bad. They’re necessary. But using them as your only layer of tests is like doing a crash test every time you want to check if the stereo works.

The crash test dummy problem

Crash testing a car is a serious operation. You strap in a dummy, set up high-speed cameras, line the car up against the barrier, and run it at full speed. It gives you critical safety data. You need it.

Now imagine you want to verify that the car’s stereo plays music. Do you set up a crash test? Of course not. You turn the radio on.

Browser automation is the crash test. When you run a Cypress test, you’re launching a browser, rendering the full UI, simulating user interactions, waiting on API calls, checking the DOM. That’s expensive, and it will always be expensive. Simulating a real user is what it is. It can’t be made lightweight.

When you use that tool to verify logic that belongs in a unit test or a component test, you’re doing a crash test to check if the stereo works. The tool is wrong for the job. Heck, I remember developers telling me they stopped running their tests because they took too long; upon inspection, I found that they had integration tests verifying whether an integer property setter rejected negative values!

The airplane

Another way I frame this is: when you’re building an airplane, you don’t do a full test flight every time you want to check one part of the system.

You test the engine in isolation first. You test the instruments separately. You verify systems together in a controlled environment. Only after you’ve confirmed the individual parts do you fly the whole thing.

The testing pyramid works the same way. Each layer has a purpose. Each one tests what it should test, at the right cost, at the right time. You do fly the plane (you do run the E2E tests), but you don’t take it up every time you want to check if the hydraulics work.

The layers and what they own

In practice, we organize tests across four layers:

  • Backend (C# / XUnit): domain logic, aggregates, event projections

  • API integration (C# / XUnit): endpoint behavior, permissions, request/response shape

  • Angular components (Jest): component rendering, interactions, isolated logic

  • End-to-end (Cypress): full user journeys, feature flows, smoke tests

The question I ask when a behavior needs a test: which layer owns this? And the answer depends on what I’m actually verifying.

If I’m checking that a POST endpoint returns the right response for a valid payload, that’s an API test. If I’m checking that a form component renders its inputs and the submit button is wired up correctly, that’s an Angular test. If I’m checking that a warehouse manager can adjust inventory and see the updated count in the UI, that might be Cypress.

The tool matches the question. Not the other way around.

E2E for every feature, but not for everything

Every feature we ship has at least one Cypress test. That’s a rule. But it’s usually one, maybe a few. The rest of the coverage lives in the layers below.

I’ve been tracking test distribution over several sprints. In the early stages of the project, we were at roughly 70% Angular tests and 20% backend tests. We were building the UI first and showing it to stakeholders before we even touched the database. As features stabilized, the backend tests grew to nearly match the frontend. The E2E line has held steady at around 5% of our total test count throughout that period.

That number isn’t an accident. It’s a deliberate constraint.

Recently, that line has ticked up a little. AI tools are making E2E tests faster to write, so the discipline of restraint is being tested. But the fundamental cost hasn’t changed. Browser automation is slow to run. That’s a physics problem, not a tooling problem.

Starting from the outside, working inward

One habit that makes this concrete is that when something breaks or we start building something new, we begin from the outside. What does the user see? What does the story say should happen?

We write the Cypress spec at the story level first. Then we refine toward the layers below as we build. Sometimes debugging stops at the Angular layer. Sometimes we have to go all the way to the database. But we start from what people see, because that’s what they describe when things go wrong.

We don’t start from the database. We never have, on this project. The database is at the end of the journey, not the beginning.

The mindset shift takes time

A team member told me recently that the conceptual part isn’t the hard part. Understanding the pyramid, understanding why each layer exists. That clicks quickly. The hard part is changing the default instinct: the pull toward the database, the reflex to start from the backend, and the tendency to reach for integration tests when a unit test would do.

I’ve helped a lot of people make that shift. Every single one of them, once they’ve made it, says the same thing: I’m not going back.

It takes time. But the shift happens, and then the whole codebase starts reflecting it: in the distribution of tests, in the speed of the suite, in the clarity of what each test is actually telling you.

Leave a Reply

Trending

Discover more from Claudio Lassala's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading