All insights
Quality engineering

Component Testing: The Layer Between Unit and Integration

Unit, component and integration are separated by one decision: what gets mocked. Where to put the boundary, and what a component test case contains.

Sep 1, 20266 min readViral Patel

A team has 4,000 unit tests and 90 end-to-end tests. The unit suite runs in 40 seconds and catches almost nothing. The end-to-end suite runs in 25 minutes and catches almost everything, usually on a Thursday afternoon when there is no time left to fix it.

The gap between those two numbers is where component testing lives, and it is the layer most teams have never deliberately decided about. They have unit tests because the framework made them easy, and end-to-end tests because somebody insisted, and nothing in between. This post covers what component testing is, the single decision that separates it from its neighbours, and what a component test case actually contains.

What component testing is

Component testing exercises one deployable unit in isolation, with its own dependencies real and everything outside it replaced.

A component here usually means a service, a module with a clear boundary, or in a front end, a UI component with its own state. The test starts the thing up properly. It uses the real database, the real routing, the real serialisation. What it does not use is any other service.

That is the whole idea in a sentence: the component is real, its neighbours are not.

The mocking boundary is the real distinction

Most articles separate unit, component and integration testing by counting how many things are involved. That is not the useful distinction, because the counts overlap and every team draws them differently.

The distinction that holds is what gets mocked.

  • Unit test: everything outside the function or class is replaced, including the database
  • Component test: the component's own dependencies are real, everything beyond its boundary is replaced
  • Integration test: two or more components are real, talking to each other
  • End-to-end test: nothing is replaced

Read that list again with your own suite in mind. Most teams find they have tests labelled "integration" that mock the database, which makes them unit tests with a slow setup. And tests labelled "unit" that spin up a real Postgres, which makes them component tests.

The labels do not matter much. What matters is knowing which assumptions each test is still making, because that tells you what class of bug it can never catch.

What this layer catches that the others do not

Component tests are the cheapest place to catch a specific and expensive class of bug: the ones that live between your code and your own infrastructure.

  • A query that is correct in the ORM and wrong in SQL once there are 50,000 rows
  • A migration that runs cleanly on an empty database and fails on one with data
  • A serialisation change that quietly turns a decimal into a float
  • A transaction boundary that does not roll back what you assumed it would
  • A unique constraint that the code path never actually respects

None of those appear in a unit test, because the database is a mock that always agrees. All of them appear in an end-to-end test, twenty five minutes later, with three other services in the trace and no obvious culprit.

Consider the fee calculation in a payments service. A unit test on the calculator confirms that 500.00 at 1.4 percent gives a 7.00 fee. It passes. The component test stores the result and reads it back, and finds the column is numeric(10,2) while the calculator returns four decimal places. The stored fee is 7.00 and the emitted event says 7.0035. Two systems now disagree by a third of a penny, on every transaction, forever.

That bug is invisible to a unit test and painful to diagnose end-to-end. It is trivial at the component layer, where the database is real and the neighbours are not.

Why teams skip this layer

Three reasons, and they are all understandable.

  • It needs infrastructure in the test run, which used to be genuinely hard and is now mostly a container
  • It is slower than a unit test, so it loses on any metric that rewards test count or speed
  • Nobody is told to write them, because the unit test is the default the framework offers

The cost of skipping shows up as a suite shaped like the one in the opening. Thousands of fast tests that assert your mocks behave the way you wrote them, and a small number of slow tests carrying all the real risk.

That shape is the reason the test pyramid gets argued about so much. The pyramid is not wrong about proportions. It is often applied by teams whose middle layer does not exist, so the choice becomes fast and useless or slow and unbearable.

What a component test case looks like

Title: A stored fee matches the emitted fee to two decimal places

Preconditions: The payments service is running against a real database instance seeded with one business tier account. The ledger service is replaced by a stub that records what it receives. The fee tier for business accounts is 1.4 percent.

Steps:

  1. Submit a transfer of 500.00 through the service API
  2. Read the persisted transaction row directly from the database
  3. Read the payload captured by the ledger stub

Expected result: The persisted fee is 7.00. The fee in the emitted payload is 7.00, not 7.0035. The two values are equal as strings, not merely as rounded numbers. Exactly one row was written and exactly one message was emitted.

The precondition doing the work is the second one. The ledger is a stub, so this case can never fail because another team deployed something. If it goes red, the bug is inside this service, and that is what makes the layer worth having.

Where to draw your own boundary

There is no universal answer, and teams genuinely disagree about where component testing starts. What matters is that your team picks one line and writes it down.

A reasonable default for a service based system:

  • Real: the service under test, its database, its cache, its own configuration
  • Stubbed: every other service, every third party, anything with its own deploy schedule

For a front end, the equivalent line is usually the network. The component and its state are real, the API is stubbed.

Write that rule next to the suite, because the alternative is that every engineer draws it slightly differently and the suite becomes unreadable within a year. Recording it alongside the cases themselves is ordinary test case management work, and it is what keeps the layer meaningful.

The wider map of testing types covers how this layer relates to the ones above and below it.

Questions people ask

What is the difference between component testing and integration testing?

A component test runs one component with its neighbours stubbed. An integration test runs two or more real components talking to each other. The line is whether anything on the far side of your boundary is real.

Is component testing the same as module testing?

The terms are used interchangeably in most teams, and neither has a settled industry definition. What matters is agreeing internally what is real in a given test, rather than which of the two words you use for it.

Should component tests use a real database?

Yes, in almost all cases. The database is the component's own dependency, and most of the value of this layer comes from catching the disagreements between your code and your schema. A mocked database returns component tests to being slow unit tests.

How many component tests should we have?

More than most teams have. A practical target is one per meaningful behaviour of the service, especially anything that writes, calculates money, or enforces a constraint. If a rule matters and lives in one service, it belongs here rather than end-to-end.

Do component tests replace end-to-end tests?

No. They verify one service behaves, which is a different claim from the whole system working. They do let you keep the end-to-end suite small, which is usually where the real gain is.

Keep going

Try Tesbo, or get the next useful idea

Start building your testing workflow now, or get one practical email a month.

Get started

One email a month

What we shipped, what we learned, and the occasional infographic worth pinning. Unsubscribe in one click.