Tdd Fundamentals · Common pitfalls

2 min read
Mid-level19 min read
Rapid overview

Common pitfalls

Q: What are the most common testing anti-patterns?

A: The Liar (tests with no meaningful assertions), brittle tests that verify implementation details, the Giant (one test covering too many behaviors), excessive setup indicating too many dependencies, shared mutable state causing order-dependent failures, and copy-paste tests that become a maintenance burden. The cure is testing behavior over implementation, keeping tests focused, and using builders/fixtures to reduce duplication.

Q: What makes a test brittle?

A: A brittle test breaks when you refactor production code without changing behavior. The most common cause is testing implementation details rather than observable outcomes:

// BAD: testing exact method call sequence — any refactoring breaks these tests
mockRepo.Verify(r => r.OpenConnection(), Times.Once);
mockRepo.Verify(r => r.BeginTransaction(), Times.Once);
mockRepo.Verify(r => r.SaveAsync(order), Times.Once);
mockRepo.Verify(r => r.CommitTransaction(), Times.Once);
mockRepo.Verify(r => r.CloseConnection(), Times.Once);

// BETTER: test the observable outcome
var savedOrder = await repo.GetByIdAsync(order.Id);
savedOrder.ShouldNotBeNull();
savedOrder.Status.ShouldBe(OrderStatus.Confirmed);
Q: What does "testing implementation details" look like?

A:

// BAD: asserting on internal data structure via reflection
Assert.Equal(3, sut._internalCache.Count); // accessing private field

// BETTER: assert on public behavior
var result = sut.GetAllCachedItems();
result.Count.ShouldBe(3);

// BAD: verifying that a specific private method was called
// (this test will break if you rename or restructure the private method)

// BETTER: verify the external effect of calling the public method
Q: What is the ice cream cone anti-pattern?

A: An inverted test pyramid with many end-to-end tests and few unit tests. Results in slow feedback, flaky CI, and hard-to-diagnose failures:

    Correct (pyramid):          Anti-pattern (ice cream cone):

         /\  E2E                    __________  E2E
        /  \                       |__________|
       /    \  Integration         |          |  Integration
      /______\                     |          |
     /        \  Unit              |__________|  Unit
    /____________\                     |  |
Q: What is The Liar anti-pattern?

A: A test that passes but does not actually verify behavior. The assertions are missing or too weak:

// BAD: The Liar — test passes but verifies nothing
[Fact]
public async Task ProcessOrder_DoesNotThrow()
{
    var sut = new OrderProcessor(new Mock<IOrderRepository>().Object);
    await sut.ProcessAsync(new Order("o1", 100m));
    // No assertions! This always passes even if the code is wrong.
}

// GOOD: actually verify the outcome
[Fact]
public async Task ProcessOrder_ValidOrder_PersistsWithConfirmedStatus()
{
    var repo = new FakeOrderRepository();
    var sut = new OrderProcessor(repo);

    await sut.ProcessAsync(new Order("o1", 100m));

    var saved = await repo.GetByIdAsync("o1");
    saved.ShouldNotBeNull();
    saved.Status.ShouldBe(OrderStatus.Confirmed);
}
Q: What is the "testing the mock" anti-pattern?

A: Verifying behavior you configured on the mock rather than on the system under test:

// BAD: you are testing Moq, not your code
var mock = new Mock<ICalculator>();
mock.Setup(c => c.Add(2, 3)).Returns(5);
Assert.Equal(5, mock.Object.Add(2, 3)); // This tests Moq itself

// GOOD: test your code that USES the calculator
var calc = new Mock<ICalculator>();
calc.Setup(c => c.Add(It.IsAny<int>(), It.IsAny<int>())).Returns(10);
var sut = new InvoiceService(calc.Object);

var invoice = sut.CalculateTotal(items);
invoice.Total.ShouldBe(10);
  • Tests that hit real databases, networks, or file systems without justification — use fakes or Testcontainers.
  • Tests that use Thread.Sleep or Task.Delay for synchronization — use TaskCompletionSource or SemaphoreSlim.
  • Tests that boot the entire application when a unit test would suffice — push most coverage to unit tests.
  • Tests that create expensive resources per test instead of sharing via fixtures — use IClassFixture<T>.
Q: What causes slow tests and how do you fix them?

A:

Q: What does excessive setup signal?

A: Fifty lines of Arrange for one line of Act signals the system under test has too many dependencies. It is a design smell — consider breaking the class into smaller, focused components. In the meantime, use builders, AutoFixture, or shared factory methods to reduce boilerplate.

Q: When should you delete a test?

A: Delete tests that test deleted features, test implementation details that change with every refactor, duplicate other tests without adding coverage, test third-party library behavior (that is their responsibility), or are permanently flaky despite attempts to fix them. Dead tests erode trust in the suite and slow CI. Regularly prune tests during refactoring sessions.


See also