Real-world examples of unit test failures: mocking issues examples for modern codebases

When developers talk about flaky tests, the conversation almost always ends up on mocking. The best examples of unit test failures: mocking issues examples usually come from real teams wrestling with brittle, over‑mocked test suites that break every time the wind changes direction. In this guide, we’ll walk through practical, code‑level examples of how mocking goes wrong, why those tests fail, and how to fix them without rewriting your entire test suite. Instead of staying abstract, we’ll look at concrete scenarios from HTTP clients, databases, time‑dependent logic, third‑party APIs, and asynchronous code. These examples of unit test failures: mocking issues examples are written for engineers who already know how to write tests, but are tired of guessing why a supposedly isolated unit test fails only in CI, or only on Tuesdays, or only after a refactor. If that sounds familiar, keep reading — you’ll probably recognize your own code in a few of these stories.
Written by
Jamie
Published

Fast tour of mocking gone wrong (with real examples)

Before getting into patterns and fixes, it helps to see what failure looks like in practice. Here are several examples of unit test failures: mocking issues examples that show up again and again in modern codebases:

  • A test passes locally but fails in CI because a network call wasn’t mocked consistently.
  • A test mocks an interface method signature that changed, so the mock no longer matches reality but still compiles.
  • A test mocks time, but only in one layer, so half the code uses Date.now() and the other half uses a fake clock.
  • A test stubs a database repository but forgets to mock a transaction boundary, so the test hits a real database in production.
  • A test mocks a third‑party SDK with unrealistic responses, so it passes while production explodes on real edge cases.

These are the kinds of examples of unit test failures: mocking issues examples we’ll unpack in detail, with code snippets and fixes.


Example of brittle HTTP client mocks that fail only in CI

Imagine a Node.js service that calls an external API:

// userService.ts
export async function getUserProfile(id: string, client = fetch) {
  const res = await client(`https://api.example.com/users/${id}`);
  if (!res.ok) throw new Error('Failed to fetch profile');
  return res.json();
}
``

A common test setup:

```ts
// userService.test.ts
import { getUserProfile } from './userService';

it('returns profile when API responds 200', async () => {
  const mockFetch = jest.fn().mockResolvedValue({
    ok: true,
    json: () => Promise.resolve({ id: '123', name: 'Jamie' }),
  });

  const profile = await getUserProfile('123', mockFetch);
  expect(profile.name).toBe('Jamie');
});

This looks fine, but here’s where the examples of unit test failures: mocking issues examples start to appear:

  • Another test in the same file does not pass mockFetch and instead relies on the global fetch.
  • Locally, node has fetch polyfilled by your test runner.
  • In CI, that polyfill isn’t available, so the test tries to call the network and fails intermittently.

Why this fails

The mocking strategy is inconsistent. Some tests inject a mock, others rely on environment magic. You now have a test suite that passes on one machine and fails on another.

How to fix it

  • Always inject dependencies like fetch into your function or service.
  • Provide a default implementation only for production wiring, not in tests.
  • In tests, always pass an explicit mock.

This is one of the most common examples of unit test failures: mocking issues examples in HTTP‑heavy microservice codebases.


Over‑specified mocks that break on harmless refactors

Over‑mocking is another classic example of unit test failures: mocking issues examples. Consider a service layer in Java or C# that calls a repository:

// UserService.java
public class UserService {
    private final UserRepository repo;

    public UserService(UserRepository repo) {
        this.repo = repo;
    }

    public UserDto getUser(String id) {
        User user = repo.findById(id);
        return UserDto.from(user);
    }
}

A typical test with Mockito:

@Test
void returnsUserDto() {
    UserRepository repo = mock(UserRepository.class);
    UserService service = new UserService(repo);

    when(repo.findById("123"))
        .thenReturn(new User("123", "Jamie", "ADMIN"));

    UserDto dto = service.getUser("123");

    verify(repo, times(1)).findById("123");
    verifyNoMoreInteractions(repo);
}

This test is fragile because it asserts on every interaction. The moment you add a harmless logging call like repo.exists(id) inside getUser, the test fails even though behavior is still correct.

Why this fails

The test mocks how the service works instead of what it should do. It is tightly coupled to internal implementation details.

How to fix it

  • Prefer verifying outcomes (return values, state changes) over interactions, unless the interaction itself is the behavior.
  • Avoid verifyNoMoreInteractions unless you truly need it.

When teams complain that “tests make refactoring painful,” they’re often staring at these over‑specified examples of unit test failures: mocking issues examples.


Time‑dependent code: fake clocks vs half‑mocked time

Time is one of the most common sources of flaky tests. Here’s a TypeScript snippet that expires tokens after 1 hour:

// tokenService.ts
export function isExpired(issuedAt: Date, now: Date = new Date()): boolean {
  const diffMs = now.getTime() - issuedAt.getTime();
  return diffMs > 60 * 60 * 1000;
}

A test might try to mock Date globally using a test framework’s fake timers, but forgets that isExpired accepts now as an argument and also uses new Date() as a default.

One test passes now explicitly. Another does not. The second test depends on the fake global clock; the first does not. You’ve created inconsistent behavior across tests.

Why this fails

This is a subtle example of unit test failures: mocking issues examples where some parts of the code use injected time, while others rely on global time. When fake timers are enabled, code that uses new Date() behaves differently from code that uses injected dates.

How to fix it

  • Pick one strategy: either inject now everywhere or consistently use a fake clock, but don’t mix.
  • At the architecture level, consider a Clock interface or a TimeProvider that can be mocked uniformly.

The testing literature has warned about this for years; Martin Fowler’s discussion of test doubles and fakes is still worth a read (martinfowler.com).


Database mocks that accidentally hit production resources

Another real‑world example of unit test failures: mocking issues examples shows up in data access tests. Imagine a Python service using SQLAlchemy:

## user_repo.py
from sqlalchemy.orm import Session

class UserRepo:
    def __init__(self, session: Session):
        self.session = session

    def get_user(self, user_id: str):
        return self.session.query(User).get(user_id)

You write a test that mocks UserRepo methods, but forget to mock the Session in another test that instantiates UserRepo directly. Locally, it works because your DATABASE_URL points to a dev instance. In CI, the environment variable is missing, so the ORM falls back to an in‑memory SQLite database. Tests pass, but they’re not actually testing what you think.

Then someone runs the tests on a machine pointed at production. Suddenly, read queries are hitting real data.

Why this fails

  • Inconsistent mocking: some tests mock UserRepo, others don’t.
  • The boundary between “unit” and “integration” is fuzzy.
  • Environment configuration is doing more work than the tests themselves.

How to fix it

  • Treat database access as an integration concern; either:
    • mock at the repository boundary consistently, or
    • run explicit integration tests against a known test database.
  • Use clear configuration for test databases; the U.S. NIST guidance on secure software development emphasizes environment isolation and configuration hygiene (csrc.nist.gov).

These are subtle but dangerous examples of unit test failures: mocking issues examples because they can mask real bugs or, worse, touch real data.


Third‑party API mocks that don’t match reality

Modern applications lean heavily on third‑party APIs: payments, messaging, auth, analytics. A classic example of unit test failures: mocking issues examples is mocking those APIs with overly optimistic responses.

Take a payment flow:

// paymentService.ts
export async function chargeCustomer(api, customerId, amountCents) {
  const res = await api.charge({ customerId, amountCents });
  if (res.status === 'DECLINED') throw new Error('Card declined');
  return res.transactionId;
}

A naive mock:

const api = {
  charge: jest.fn().mockResolvedValue({
    status: 'APPROVED',
    transactionId: 'tx_123',
  }),
};

Tests now only exercise the happy path. Real gateways, however, return:

  • Network timeouts
  • 5xx errors
  • Throttling responses
  • Weird edge statuses while they’re migrating versions

Why this fails

Your mocks are lying. They don’t resemble the real API’s behavior or error surfaces. Tests are green, but production is red.

How to fix it

  • Base mocks on API documentation and real captured responses.
  • Include negative paths: timeouts, invalid parameters, rate limiting.
  • Consider contract tests or consumer‑driven contracts for critical integrations.

This is where examples of unit test failures: mocking issues examples intersect with broader reliability practices like chaos testing and API contract validation.


Async race conditions hidden by mocking

Asynchronous code is another fertile source of examples of unit test failures: mocking issues examples. Think about a React component that loads data on mount and updates state:

// useUser.ts
export function useUser(api, id) {
  const [user, setUser] = useState(null);
  useEffect(() => {
    let cancelled = false;
    api.getUser(id).then((u) => {
      if (!cancelled) setUser(u);
    });
    return () => {
      cancelled = true;
    };
  }, [id]);
  return user;
}

A test might mock api.getUser as an immediately resolved promise:

api.getUser = jest.fn().mockResolvedValue({ id: '1', name: 'Jamie' });

In the real app, the promise resolves later, interleaving with user interactions and unmounts. In tests, everything happens synchronously, so race conditions never appear.

Why this fails

The mock’s timing characteristics don’t match production. You’re not testing cancellation or state updates that occur after unmount.

How to fix it

  • Use async utilities that more accurately model real timing, like waitFor in React Testing Library.
  • Introduce small artificial delays in mocks to catch race conditions.
  • For critical flows, add integration tests that exercise real async behavior.

These examples of unit test failures: mocking issues examples are subtle because the tests pass consistently, yet they fail to protect you from real‑world concurrency bugs.


Two current trends are making mocking failures more visible:

Microservices and distributed systems
As systems decompose into dozens of services, each team maintains its own test suite with its own mocking strategy. Inconsistent contracts, partial mocks, and version skew between services create a growing set of examples of unit test failures: mocking issues examples that only surface when services talk to each other.

Increased use of AI‑generated code and tests
More teams are experimenting with AI tools to generate boilerplate tests. Those tools often over‑mock internals or create unrealistic stubs, amplifying the very examples of unit test failures: mocking issues examples we’ve been talking about.

If you want a grounding in software testing fundamentals, classic academic and industry sources are still valuable. For instance, Carnegie Mellon’s Software Engineering Institute regularly publishes guidance on software assurance and testing practices (sei.cmu.edu).


Patterns that prevent mocking‑related unit test failures

Pulling the threads together from these real examples, a few patterns consistently reduce mocking pain:

Favor dependency injection over globals

When dependencies are passed in explicitly (HTTP clients, time providers, loggers, repositories), it becomes much easier to:

  • Mock consistently
  • Swap implementations for tests vs. production
  • Avoid hidden environment dependencies

Mock behavior, not implementation details

Your tests should describe what the unit does:

  • Inputs → outputs
  • State changes
  • Observable interactions (like sending an email)

They should avoid describing how it gets there (exact number of method calls, internal helper usage) unless that behavior is the contract.

Use fakes and in‑memory implementations where possible

Instead of mocking every method, consider:

  • In‑memory repositories
  • Local HTTP servers for API tests
  • Embedded databases for integration tests

These often provide more realistic behavior with less mocking ceremony.

Maintain realistic test data and contracts

Use:

  • Realistic JSON payloads captured from staging
  • Schema validation and contract tests
  • Versioned API contracts shared between teams

This helps avoid the “happy‑path only” examples of unit test failures: mocking issues examples that let bugs slip into production.


FAQ: common questions about mocking and unit test failures

Q: Can you give another simple example of a mocking issue that causes unit test failures?
A: A classic example of mocking gone wrong is stubbing a method to return null in a test when the real implementation never returns null. Your production code doesn’t handle null, because it never sees it. The test passes, but you’ve trained the code against an impossible scenario, and you still miss the real bug.

Q: How many mocks are too many in a unit test?
A: If a single test needs to mock five or six collaborators, that’s a smell that your “unit” might actually be a small integration. Consider testing a smaller surface or introducing a thin orchestrator that can be tested with fewer doubles.

Q: Are there examples of when I should prefer integration tests over heavy mocking?
A: Yes. Whenever behavior crosses a meaningful boundary — database transactions, external APIs, message queues — a small, well‑scoped integration test is often more reliable than a heavily mocked unit test. It reduces the number of examples of unit test failures: mocking issues examples where mocks drift away from reality.

Q: How do I know if a flaky test is caused by mocking?
A: Look for patterns: tests that fail only in CI, only under load, or only when run in parallel often involve timing or environment assumptions baked into mocks. Temporarily logging real calls or running the test suite with different environments can reveal mocks that depend on hidden state.

Q: Where can I learn more about good mocking practices?
A: In addition to industry blogs, academic software engineering courses from universities like MIT and Carnegie Mellon cover testing and test doubles in more depth. For example, MIT OpenCourseWare includes software engineering materials that touch on testing strategies (ocw.mit.edu).


If you recognize your own code in these stories, that’s a good thing. These examples of unit test failures: mocking issues examples aren’t a sign that you’re bad at testing; they’re a sign that your system is complex enough to deserve better testing patterns. Start by standardizing how you inject dependencies, dialing back over‑specified mocks, and making your test doubles look a little more like the real world your code actually runs in.

Explore More Unit Testing Failures

Discover more examples and insights in this category.

View All Unit Testing Failures