Should I test that?

Should integration tests use mocks?

Answer

Yes, integration tests should use mocks only for services you do not run: replace each third-party API with a fake server that returns recorded responses, including errors, and run your database and your own code for real.

Verdict on the code under testYes

Why

Yes, but only for services you do not run: put a fake HTTP server in place of each third-party API, and run your database and your own modules for real. The typical case is a backend whose integration tests run against a real database, while some endpoints call an email or shipping provider. For the code on your side of that API, Blast radius is users and Change frequency is regularly, because the requests change with features about once a month. Detectability is eventually, because a misread response becomes an empty value that looks plausible, and Reversibility is with-effort, because records saved with that value need a repair script. Test cost is moderate, because a fake server with recorded responses takes me about an hour per endpoint, so rule R11 gives Test.

When the decision changes
WhenDecisionWhy
The call charges a customer's card through a payment providerTest mandatory: a recorded response for each outcome, including a decline and a timeout, plus a run in the provider's test mode before each releaseBlast radius rises to money and Reversibility to costly, because a double charge ends in a refund
The code sends a customer ID to a document storage API and shows the files it returnsTest mandatory: a test with two customers that asserts the ID in each request the fake server receivesBlast radius rises to safety-or-legal and Detectability to never, because another customer's files look like normal output
The failure to catch is a change in the provider's response formatTest it differently: send the recorded requests to the provider's sandbox daily, and alert on production responses that fail to parseTest cost rises to heavy, because only the real provider shows the change, and its sandbox is slow and rate-limited
The call only reads a parcel's tracking status for display, and a failure shows an error that users reportTest minimally: one test with a fake server that returns the main responseDetectability moves to same-day and Reversibility to trivial, because a read stores nothing
The call posts to your team's chat when a nightly job finishesDo not test the call; check the chat after you change it, and the team notices a missing message the next morningBlast radius falls to internal, Detectability moves to same-day and Reversibility to trivial

What breaks if you don't test

Without a test against the provider's responses, the error paths first run in production. A batch endpoint answers HTTP 200 with an error for each failed item, a rate limit answers 429, and code tested only against the success response treats both as success. A customer notices the missing email days later.

What you lose if you over-test

Calling the real provider from CI ties every build to its sandbox: rate limits and outages fail builds for reasons outside your code, and a live key can send real email. Mocking your own database fails the other way: the mock returns what the test hands it, so the SQL and the column mapping never run. A hand-written mock that knows only the success response gives the same false confidence about a third-party API.

How to test

  1. Run your own dependencies for real, such as the production database engine in a container started by Testcontainers.
  2. Replace each third-party API at the HTTP boundary with a fake server such as Mock Service Worker, set to fail on any request it has no handler for.
  3. Record responses from the provider's sandbox as fixtures, including each error the code handles.
  4. Assert the request your code sends as well as what it saves.
  5. Send the recorded requests to the real provider once a day, outside the build, as a contract test.

When the answer changes

  • The call moves money or returns records that only one customer may see.
  • The provider changes its responses without notice.
  • The call only reads data for display, and users report a failure the same day.

Real incident + Code example

The replies that were never sent

On a helpdesk product I worked on, agents' replies went out through Postmark's batch endpoint. The integration tests used a hand-written mock that answered HTTP 200 with an empty array, and the code marked every reply as sent after any 200. The real endpoint returns per-message error codes inside an HTTP 200 response, with code 406 for an inactive recipient. Replies to those recipients showed "sent" in the ticket, and a customer complained 12 days later that none of his tickets had an answer. The fix came with this test:

// Vitest: real PostgreSQL from Testcontainers, the email provider replaced by MSW.
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, beforeAll, expect, test } from "vitest";
import recorded from "./fixtures/batch-one-inactive.json"; // ErrorCode 406 for message 2

const server = setupServer(
  http.post("https://api.postmarkapp.com/email/batch", () => HttpResponse.json(recorded)),
);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterAll(() => server.close());

test("a reply to an inactive recipient is not marked as sent", async () => {
  const ids = await queueReplies(db, ["active@acme.test", "bounced@acme.test"]);

  await sendQueuedReplies(db);

  const { rows } = await db.query(
    "SELECT status FROM replies WHERE id = ANY($1) ORDER BY id", [ids]);
  expect(rows.map((r) => r.status)).toEqual(["sent", "not_delivered"]);
});

FAQ

Should I mock APIs in end-to-end tests?

No, an end-to-end test should run against your own backend API, because the seam between frontend and backend is what the test checks. Replace only third-party APIs you do not run, such as a maps or payment provider, with a fake server or the provider's test mode.

Should I mock the database in integration tests?

No, an integration test should run against the production database engine, because a mock of the database never runs the SQL or the column mapping. Testcontainers starts that engine in a container for each test run.

Is a test with mocks still an integration test?

A test that mocks only third-party services is still an integration test, because your code, your database and your framework run together in it. A test that also mocks your own database checks only the wiring between your classes, and it cannot find wrong SQL.