Mocking Unit Tests

 

Why I Avoid Mocking the Database in Unit Tests



A green test should mean something. When a unit test mocks the database, it often doesn't.

Mocks let a test pass even when the real thing is broken. That is the core problem. Here are three ways it shows up.

1. The test verifies the mock, not the code

Say you replace a database call with a stub:

db.select = vi.fn().mockResolvedValue([{ id: "abc" }]);

Now your test proves the function called the stub in the right order. It does not prove:

  • the SQL is valid
  • the schema actually has that column
  • the where clauses return the right rows on real data
  • soft-delete or tenant filters are applied
  • the transaction rolls back when it should
  • a lock really prevents the race condition you added it for

You get a green check that means almost nothing.

2. Mock drift: the silent decay

A module mock only exposes the exports you listed when you wrote it. Months later, production code starts importing something new from the same module: a new operator, a new column, a new helper. The mock returns undefined for it, and the next run fails with "No 'X' export is defined on the mock" instead of the real assertion failure you would have seen with the real module.

By then the test had already stopped doing what you thought it did. It just hadn't told you.

3. Wrong tool for the layer

Code that talks to a database needs a test that talks to a real database: an integration or API-level test running against a real Postgres, real constraints, real transactions. Mocking the database in a unit test tries to do that job with unit-level tools. You pay the cost of writing the mocks and get none of the real-contract verification.

A rule I like: if a test needs a running database, an HTTP request, or a real browser, it isn't a unit test.

What unit tests are actually for

Pure logic: rules, decisions, transformations, edge cases. Anything where you can say "given these inputs, expect this output" without time, network, or external state.

This shapes how you write code. Pull the decisions out of the database-touching code into small pure functions ("is this start time inside the allowed window?", "is this request a replay of an earlier one?", "which duplicate records do I drop?"). Unit-test those thoroughly. Let the higher-tier tests verify that the orchestrating code, which does the database work, wires them up correctly.

When mocking is okay

Not banned, just narrow:

  • Modules with side effects on import, like config loaders. Mock them just so the test file can load. You're not claiming to verify the config.
  • A clean boundary, like "add this job to a queue", but only if a higher-tier test verifies the real thing still works. Otherwise you're back to false confidence.

My short version: mock to enable a test, never to substitute for one.

The trade-off

Real-database tests are slower and need setup, which is why teams reach for mocks. The answer isn't to skip them. Keep the fast unit layer for pure logic, and put the database-facing tests where they belong, run less often but against the real thing.

Comments

Popular posts from this blog

SOFTWARE QA TESTING FOR AI-POWERED APPLICATIONS

Test Case Design Techniques

ARTIFICIAL INTELLIGENCE IN SOFTWARE TESTING