Tdd Fundamentals · Quick recall Q&A
3 min readQuick recall Q&A
The testing pyramid has many fast unit tests at the base, fewer integration tests in the middle, and a small number of end-to-end tests at the top. This structure optimizes for fast feedback (unit tests run in milliseconds), targeted wiring verification (integration tests), and confidence in critical user journeys (E2E). Inverting the pyramid (the ice cream cone) results in slow CI, flaky tests, and hard-to-diagnose failures.
| Consideration | Classicist | Mockist |
|---|---|---|
| Refactoring resilience | High | Lower |
| Design pressure | Moderate | High (pushes small classes) |
| Setup complexity | Can be higher (real objects) | Can be higher (mock configuration) |
| Best for | Domain logic, algorithms | Interaction-heavy orchestration |
I use classicist (real objects, fakes) for domain logic and algorithms where state verification is natural and refactoring resilience matters. I use mockist (mocks, interaction verification) for orchestration layers and infrastructure boundaries where verifying that the right calls happened is the core behavior. Most production teams blend both styles depending on the layer they are testing.
// Classicist: use a real in-memory repository
[Fact]
public async Task PlaceOrder_ValidOrder_PersistsToRepository()
{
var repo = new FakeOrderRepository();
var sut = new OrderService(repo, new RealPriceCalculator());
await sut.PlaceOrderAsync(new Order("o1", "AAPL", 10));
var saved = await repo.GetByIdAsync("o1");
saved.ShouldNotBeNull();
saved.Status.ShouldBe(OrderStatus.Placed);
}
// Mockist: verify interactions with mocked dependencies
[Fact]
public async Task PlaceOrder_ValidOrder_CallsRepositoryAndCalculator()
{
var repo = new Mock<IOrderRepository>();
var calc = new Mock<IPriceCalculator>();
calc.Setup(c => c.Calculate(It.IsAny<Order>())).Returns(150m);
var sut = new OrderService(repo.Object, calc.Object);
await sut.PlaceOrderAsync(new Order("o1", "AAPL", 10));
repo.Verify(r => r.SaveAsync(It.Is<Order>(o => o.Id == "o1")), Times.Once);
calc.Verify(c => c.Calculate(It.IsAny<Order>()), Times.Once);
}Abstract the clock behind an interface like ISystemClock or TimeProvider (introduced in .NET 8). Inject it as a dependency. In tests, provide a fake clock that returns a fixed or controlled time. This makes tests deterministic.
public class FakeTimeProvider : TimeProvider
{
private DateTimeOffset _now;
public FakeTimeProvider(DateTimeOffset startTime) => _now = startTime;
public override DateTimeOffset GetUtcNow() => _now;
public void Advance(TimeSpan duration) => _now = _now.Add(duration);
}
// Usage in a test
[Fact]
public void TokenIsExpired_WhenCurrentTimeExceedsExpiry()
{
var clock = new FakeTimeProvider(new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero));
var token = new AuthToken(
expiresAt: new DateTimeOffset(2025, 6, 15, 11, 0, 0, TimeSpan.Zero));
var sut = new TokenValidator(clock);
sut.IsExpired(token).ShouldBeTrue();
}
[Fact]
public void TokenIsNotExpired_WhenCurrentTimeBeforeExpiry()
{
var clock = new FakeTimeProvider(new DateTimeOffset(2025, 6, 15, 10, 0, 0, TimeSpan.Zero));
var token = new AuthToken(
expiresAt: new DateTimeOffset(2025, 6, 15, 11, 0, 0, TimeSpan.Zero));
var sut = new TokenValidator(clock);
sut.IsExpired(token).ShouldBeFalse();
}Property-based testing generates hundreds of random inputs and verifies that invariants hold for all of them. It is more effective for mathematical operations (commutativity, associativity), serialization roundtrips, parsers, sorting algorithms, and any code with clear invariants. FsCheck is the primary .NET library.
using FsCheck;
using FsCheck.Xunit;
public class SortingProperties
{
[Property]
public Property Sort_PreservesLength(List<int> input)
{
var sorted = input.OrderBy(x => x).ToList();
return (sorted.Count == input.Count).ToProperty();
}
[Property]
public Property Sort_OutputIsOrdered(List<int> input)
{
var sorted = input.OrderBy(x => x).ToList();
var isOrdered = sorted.Zip(sorted.Skip(1), (a, b) => a <= b).All(x => x);
return isOrdered.ToProperty();
}
}Maximize unit tests (milliseconds each), minimize integration tests to critical wiring paths, parallelize test execution (xUnit does this by default), use in-memory fakes instead of real databases where possible, avoid Thread.Sleep/Task.Delay, share expensive fixtures with IClassFixture, and run heavy integration suites on separate CI stages rather than on every push.
Use the Builder pattern or AutoFixture to construct objects with sensible defaults and override only what matters for each test. Centralize builders alongside the domain model so they evolve together. Avoid constructing complex object graphs inline in every test.
Use unique resource identifiers (database names, queue topics, blob prefixes) per test run, isolate shared state through fixtures, and ensure teardown cleans resources. Mark collection fixtures to avoid serial bottlenecks and rely on containerized dependencies to avoid cross-test interference.
Attach in-memory exporters for OpenTelemetry during integration tests, trigger key user journeys, and assert on emitted spans/metrics/logs (names, attributes, and error flags). This ensures dashboards and alerts stay trustworthy without requiring external telemetry backends.