The pull request is a sea of glorious green checkmarks. Your CI/CD pipeline is singing. The sonar report proudly declares: 95% Code Coverage. You feel like an absolute engineering god. You merge the code, deploy it to production, pack your bags, and head out for a nice weekend.

Saturday at 4:00 PM, your phone buzzes. Production is down.

A standard SQL query is throwing a syntax error because of a broken Entity Framework translation. A foreign key constraint failed because an ID was missing. And a third-party API call timed out, causing the entire container loop to crash.

You sit at your laptop, bewildered. “But the unit tests passed! I covered that exact service layer!”

Yes, you did. But you covered it with mocks. You didn’t test your code; you tested a fictional version of the universe where your database never fails, your queries are always syntactically perfect, and external networks are flawless.

In the developer community, we’ve developed a dangerous obsession with vanity metrics. We treat code coverage percentages like a high score in a video game, and we use mocking frameworks to cheat our way to the top. It’s time to kill the vanity metrics, face reality, and write tests that actually stop your app from crashing.

1. The Mocking Narcissism: Testing Your Assumptions, Not Your Code

Mocking was originally designed to isolate your code from things that are genuinely hard to control, like an external SMS gateway or the system clock.

But over time, we got lazy. We started mocking everything that required more than two lines of setup. Today, a typical “unit test” for a business service looks something like this:

C#

var mockRepo = new Mock<IUserRepository>();

mockRepo.Setup(r => r.GetByIdAsync(1).ReturnsAsync(new User { Id = 1, Name = “Alice” });

var service = new UserService(mockRepo.Object);

var result = await service.GetAdultUserAsync(1);

Assert.NotNull(result);

Think about what this test is actually doing. You are telling the mock repository exactly what to return when it receives the number 1. Then you pass that mock to your service, call the method, and assert that it returned the exact object you told the mock to create.

This isn’t a test. This is an echo chamber.

You are testing your own configuration. If your underlying SQL query in the real repository has a missing comma, or if the database column type doesn’t match your C# entity, this unit test will never catch it. You are completely blind to the infrastructure layer, which is exactly where most real-world bugs live.

2. The Big Green Lie of 95% Coverage

Code coverage is a vanity metric because it only measures execution, not correctness.

If a test executes a line of code, that line turns green in your coverage report. The tool doesn’t care if the data flowing through that line is a mocked hallucination.

You can easily achieve 100% code coverage on a repository layer by mocking the database context. But the moment that code hits a live SQL database, it might blow up because of an unsupported LINQ-to-SQL translation. Your coverage tool lied to you, and you believed it because it made your PR look good.

When managers demand high coverage metrics, developers respond by mocking out the hard stuff. It is infinitely easier to write 50 mock unit tests that run in milliseconds than to set up a proper environment that tests real behavior. We end up prioritizing the speed of our test suite over the safety of our production environment.

3. Real Production Has Teeth (And Mocks Don’t)

Mocks create an idealized vacuum. But your software doesn’t live in a vacuum; it lives on a messy network, attached to a strict database, dealing with real infrastructure constraints.

When you mock out your database or your message broker, you miss out on testing:

  • Database Constraints: Nullability violations, duplicate key exceptions, and foreign key failures.
  • Transaction Rollbacks: What happens to your system state if step three of a database operation fails? Does your mock properly simulate a broken transaction? (Usually, no).
  • Data Truncation: What happens when your code tries to save a 500-character string into a VARCHAR(100) column? Your mock will accept it happily. Your live database will throw an exception.

If your testing strategy doesn’t validate how your code interacts with these real-world blockers, your tests are lying to you. They are giving you a false sense of security that crumbles the second real users start hitting your endpoints.

4. The Alternative: Integration Testing Is Cheap Now

Ten years ago, avoiding mocks was painful. Setting up a local database for testing meant installing SQL Server on your machine, managing local connection strings, and manually running cleanup scripts after every test run to keep the state clean. It was slow, brittle, and a total nightmare.

But it is 2026. The tooling has evolved, and there is no longer an excuse for over-mocking.

With modern ecosystems like Testcontainers and .NET’s WebApplicationFactory, you can spin up a real, completely isolated instance of PostgreSQL, Redis, or RabbitMQ inside a lightweight Docker container directly from your test setup.

C#

// Spinning up a real database for a test takes one line now

var postgresContainer = new PostgreSqlBuilder().Build();

await postgresContainer.StartAsync();

Your integration tests can now hit a real database instance, run your actual database migrations, execute your real SQL queries, and validate real constraints, all within a couple of seconds.

By testing the real interaction between your code and your infrastructure, you catch the silent killers, like bad indexing, mapping issues, and serialization bugs, long before your code gets anywhere near a deployment pipeline.

Learning how to balance these fast integration tests against lightweight unit tests is one of the most critical skills a developer can learn. This shift from academic “math-like” unit testing to production-grade automation is exactly why platforms like Dometrain have built dedicated, deep-dive paths on testing. If you want to stop writing tests that just tick a box and start building a safety net that protects your uptime, you have to move past basic mocking tutorials.

5. The ‘When to Mock’ Rulebook

We aren’t saying mocking is evil. It has a place. But it should be your last resort, not your default choice.

If you want to build a test suite that matters, use this simple rulebook for when to use a real implementation versus when to use a mock:

DO NOT MOCK (Use Real Infrastructure)

  • The Database: Use Testcontainers to run tests against a real instance of your database. Test your queries, your migrations, and your constraints.
  • Internal In-Memory Logic: Domain models, validation rules, mapping logic, and algorithms should always be tested natively.
  • The Message Broker: If your app relies heavily on RabbitMQ or Kafka, spin up a local container to ensure your events are actually serializing and routing correctly.

OKAY TO MOCK (The Valid Use Cases)

  • Third-Party APIs You Don’t Control: If you pay per request for an SMS provider (like Twilio) or a payment gateway (like Stripe), mock it. You don’t want your test suite draining your company’s bank account.
  • The System Clock: If your business logic depends on time (e.g., “Is this order older than 30 days?”), abstract the clock behind an interface and mock it so your tests are deterministic.
  • Unpredictable Failure States: If you need to test how your app behaves when a network completely drops mid-stream, a mock can help you safely force that specific error path.

The Verdict: Quality over Percentages

A 60% code coverage suite packed with real integration tests that touch real databases is worth infinitely more than a 95% coverage suite built entirely on fake mocks.

Stop writing tests to please your manager’s dashboard or to make your pull requests look impressive. Your users don’t care about your code coverage reports; they care if the application works when they click a button.

Ditch the mocking narcissism. Put down the setup scripts for a minute, embrace tools like Testcontainers, and start writing tests that actually replicate the harsh, unpredictable reality of production. Your safety net should be made of steel, not assumptions.

Author Bio

Nick Chapsas

Founder and Educator at Dometrain

Nick Chapsas is a .NET and C# educator, content creator, and Microsoft MVP for Developer Technologies. He is the founder of Dometrain, a platform offering practical, high-quality courses for developers. With years of experience in software engineering and management, Nick has built systems serving millions of users and now shares his expertise through YouTube and the Keep Coding Podcast.