The #1 Mistake in Unit Testing (and How to Fix It)
Best practices to nail the most important part of your unit tests
CodeRabbit – AI Code Reviews That Actually Help (Sponsor)
As someone who’s reviewed thousands of pull requests, I can confidently say that CodeRabbit gets it right.
It doesn’t just flag lint rules. It understands your code structure using AST, explains the “why” behind each suggestion, and summarizes complex changes clearly for both reviewers and authors.
Top 5 reasons why you should try CodeRabbit and use it every day:
Context-aware reviews: It knows how your changes impact the full system, not just the diff.
Security and performance insights: Detects issues like vulnerabilities and slow patterns (e.g. sequential DB calls).
Custom team learning: Learns from your feedback and adapts reviews to your team's coding standards.
PR summaries and diagrams: Generates clear explanations, changelogs, and sequence diagrams.
Open source friendly: Free forever for public repos. No catch. CodeRabbit Pro is free forever.
Try CodeRabbit on your next PR -> no card needed, no BS. Just better reviews.
Motivation
Clean code isn't just for production code. Your tests deserve the same care. And when it comes to tests, nothing matters more than assertions. They’re the most important part of your tests. The final word, the “proof” that your system works.
Yet in many codebases, I see poorly written assertions. Sloppy assertions kill readability, bury bugs, and make tests hard to maintain.
Here are 5 tips for writing clean, expressive assertions that future-you and your teammates will thank you for.
Assert One Behaviour Per Test
Every test should verify a single, observable behavior. That doesn’t mean one physical assertion. It means one logical assertion. Sometimes, confirming one behavior might require multiple assertion lines, and that’s totally fine.
But if your test is checking multiple unrelated outcomes, it's doing too much. This hides intent and makes failures harder to diagnose. Keep each test focused on one behavior.
Rule of thumb: When in doubt, split your tests into multiple test cases.
Capture Domain Knowledge with Extract Method
Tests can teach, but only if they speak the language of your domain. One of my favorite techniques is to extract logic-heavy conditions into well-named helper functions.
Software is ultimately about solving problems in a specific business domain. Capturing that domain makes your code more expressive and meaningful.
Let each test tell a story:
Use Fluent Assertion Libraries
Think of your tests as documentation. They must be clear, readable, and concise. If your assertions are not fluently readable English sentences, then you are doing it wrong.
Fluent assertion libraries like FluentAssertions or AssertJ help you write tests that read like natural language. This not only improves readability but also helps express intent more clearly.
For example, the following assertion is hard to read and leads to a generic failure message:
❌ Assert.True(orders.contains(orderWithId(42)))
But the following assertion reads like plain English, and expresses exactly what you're checking:
✅ orders.shouldContain(orderWithId(42));
Fluent assertions also make debugging faster by showing meaningful output when tests fail.
Avoid Logic In Assertions
Avoid logic in your test code!
if-else statements
for and while loops
switch cases
They can lead to bugs in your tests.
Once you feel the need for logic, it's a smell that you test more than one thing. You can get rid of test logic by splitting up your test into multiple test cases. Tests should be the simplest part of your code base.
Assert Outcomes, Not Implementation
Focus on what the system does, not how it does it. When tests are tightly coupled to implementation details, they become fragile. Every internal refactor risks breaking tests, even when the behavior hasn't changed. That’s a maintenance nightmare.
Instead, write tests that assert observable outcomes. Focus on what happens, not how it happens.
Bad example - asserting implementation:
❌ mockRepo.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
Good example - asserting outcome:
✅ order.Status.Should().Be(OrderStatus.Confirmed);
If your implementation changes but the behavior stays the same, your tests should still pass. That’s how you build confidence.
Conclusion
Assertions are the heartbeat of your tests. They prove and document if your system works. Clear, focused assertions make your tests readable, reliable, and resilient to change.
Avoid testing internals, skip the logic, and speak the language of your domain.
Sloppy assertions = unreadable tests
Clean assertions = living documentation
Write code for humans to read, not just for machines to execute.
Sponsorships
There are still a few sponsorship slots available for the coming months. If you want to sponsor this newsletter, apply here:
Nice newsletter. However, keep in mind one point: .NET Fluent Assertions is now a paid library. You can still use its old version for free or switch to a free alternative - Shouldly.
"Write code for humans to read, not just for machines to execute." Super!