
Unit tests verify individual units of an application in isolation and in milliseconds – without a database or infrastructure. This article shows how the AAA pattern (Arrange, Act, Assert) keeps tests readable and easy to diagnose, and breaks down the different test doubles: dummy, stub, spy, mock, and fake. One focus is on common anti-patterns such as over-mocking, brittle tests, assertion roulette, mystery guest, and test logic inside the test code. It also explains why code coverage is a useful indicator but not proof of quality. Bottom line: well-written unit tests improve design, make refactoring safer, and catch flawed implementations early.
A unit test is an automated test that verifies a single, self-contained unit of the system – such as a method – in isolation from its dependencies. It runs in milliseconds and needs no database, no network access, and no running infrastructure.
Well-written unit tests deliver the following benefits:
BorrowBookAsync_BookNotAvailable_ThrowsBookNotAvailableException precisely describe what's supposed to happen in that scenario.
Every unit test can be broken down into three phases. This separation makes tests more readable and makes it easier to diagnose a failure.
A good example of AAA:
[TestMethod]
public void Create_ValidInputs_SetsDueDate14DaysLater()
{
// Arrange
var now = new DateTimeOffset(2026, 1, 15, 12, 0, 0, TimeSpan.Zero);
var expectedDueDate = now.AddDays(14);
// Act
var loan = Loan.Create(Guid.NewGuid(), Guid.NewGuid(), now);
// Assert
loan.DueDate.ShouldBe(expectedDueDate);
}A bad example of AAA:
[TestMethod]
public void TestLoan()
{
var loan = Loan.Create(Guid.NewGuid(), Guid.NewGuid(),
DateTimeOffset.UtcNow); // not deterministic!
Assert.IsNotNull(loan);
Assert.AreEqual(loan.BorrowedAt.AddDays(14), loan.DueDate);
Assert.IsNull(loan.ReturnedAt);
// what is actually being tested here?
}
When every method of a stub is verified, you're no longer testing individual behavior but the system's implementation.
Example:
// Too much: implementation detail
await _books.Received(1).GetByIdAsync(book.Id); // internal query
await _books.Received(1).UpdateAsync(book); // this is relevant!
// Only verify business-relevant interactions.Tests that rely on internal structure (private fields) break during refactoring. You should test behavior, not implementation.
A test with twelve asserts only shows, on failure, which assert failed first. In that case, the test should be split up.
The data comes from an external file, a global variable, or a database. This makes it harder for the reader of the test to understand why a particular value is used.
Control flow inside a test can introduce bugs into the test itself. Tests should be kept simple. Different variants can be modeled using DataRow.
Example:
// NOT LIKE THIS
[TestMethod]
public void TestTitles()
{
foreach (var title in new[] { null, "", " " })
{
// Test logic = potential bugs in the test itself!
}
}// LIKE THIS
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
public void Constructor_EmptyTitle_ThrowsArgumentException(string? title) { ... }Code coverage indicates what proportion of the program code was executed during the tests, but it says nothing about the quality of the tests.
What code coverage tells you: which code hasn't been tested at all.
What code coverage doesn't tell you: whether the tests are meaningful, whether the right scenarios are covered, or whether the assertions are correct. That said, a coverage report can help identify classes or code paths that aren't tested at all.
Unit tests are an important part of modern software development. They help uncover flawed implementations – ones that could, for example, damage existing code – much faster.

Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.
Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.