4 Comments
User's avatar
Jonathan Búcaro's avatar

Hi, firat of all, great article, thank you for sharing it.

I'm a bit confused. So, these Acceptance Tests are like Integration Tests but with a focus on business logic? What if the service that we are trying to consume is a paid one, should we mock it?

Valentina Jemuović's avatar

Hi Jonathan, good questions!

On acceptance tests vs integration tests:

First, it helps to split "integration tests" into two categories, because the term is overloaded:

- Narrow integration tests verify one boundary in isolation — e.g., "does my repository talk to the database correctly?" or "does my HTTP client parse the response correctly?" External systems (and their transitive dependencies) are replaced with stubs or in-memory equivalents.

- Broad integration tests wire up many components together and exercise a large slice of the system at once — pulling in real external systems and their transitive dependencies (so if your service calls an API that calls another API that hits a database, all of that runs for real).

Acceptance tests overlap with broad integration tests in scope — they also run through many components — but with two key differences:

- Intent: broad integration tests ask "do these components work together?" (technical focus). Acceptance tests ask "does this business feature behave correctly from the user's perspective?" (business focus).

- External systems: broad integration tests often hit real external systems and everything downstream of them. Acceptance tests deliberately stub them at the boundary, so tests stay fast, deterministic, and free of third-party costs or outages — regardless of how deep the transitive dependency chain goes.

The DSL in the article (given().country().withTaxRate("8%")) is the giveaway — it reads like a specification, not a technical assertion. That's what makes it an acceptance test rather than a broad integration test, even though mechanically they exercise similar code paths internally.

Narrow integration tests still have their place — e.g., testing TaxGateway against a stubbed HTTP server to verify the JSON parsing. That would have caught the bug in the article directly. But in practice most teams don't write them for every gateway, which is why acceptance tests are the net that catches these bugs in real projects.

====================================

On paid external services:

Yes — you should always stub them in acceptance tests. You don't want to pay per test run, and you don't want your CI depending on a third party being up.

The way to stay safe is the third layer I mentioned: contract tests. These run the same scenarios against the real (paid) service and against your stub, and verify they behave the same. You run them less frequently — maybe nightly, or on-demand, or only when the external API publishes a new version — so the cost stays manageable. Many paid services also offer a sandbox/test mode that's free or much cheaper for exactly this purpose.

So the pattern is:

- Acceptance tests → stub (fast, free, run on every commit)

- Contract tests → real service in sandbox mode (slow, maybe costs a little, run occasionally)

That way you get fast feedback on every change, and you still catch the day the paid service changes its response format.

Jelena Cupac's avatar

How do you convince your team to add acceptance tests when they say “we already have too many tests”?

Valentina Jemuović's avatar

"Too many tests" is rarely about quantity — it's about the wrong tests:

slow, brittle, distrusted ones that don't trace back to anything the

business asked for.

Unit tests give you fast inner-loop feedback, but they don't tell you

whether the feature works from the user's perspective. You can have 100%

unit coverage and still ship a broken flow.

The reframe: tests should trace back to acceptance criteria — one per

behavior the business actually asked for. That's the right number by

construction, neither too many nor too few. Acceptance tests make that

mapping visible, and they replace the flaky end-to-end UI tests trying to

do the same job through the browser.

The move: propose a trade — one acceptance test tied to criteria, delete

the flaky end-to-end ones it obsoletes. Keep the unit tests.