Tdd Fundamentals · Quick recall Q&A
3 min readRapid overview
Quick recall Q&A
Q: Explain the testing pyramid and why it matters. 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.
// 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);
}
| 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 |
Q: How do you decide between classicist and mockist TDD? A: 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.
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();
}
Q: How do you test code that depends on the current time? A: 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.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();
}
}
Q: What is property-based testing, and when is it more effective than example-based testing? A: 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.
Q: How do you keep tests fast in a large codebase? A: 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.Q: How do you handle test data setup for complex domain objects? A: 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.
Q: How do you keep integration tests parallelizable without flakiness? A: 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.
Q: How do you validate observability instrumentation through tests? A: 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.