๐Ÿงช

Unit Testing at Scale (EN)

Testing Intermediate 4 min read 600 words

Tech Lead / Principal-level unit testing: design-for-test, patterns, smells, boundaries, and maintainable test suites

Unit Testing at Scale (EN)

Unit tests are not about โ€œtesting everythingโ€ โ€” they are about creating a fast, reliable feedback loop that protects behavior while enabling change. At Tech Lead/Principal level, the goal is a test suite that is:

  • Fast (seconds/minutes, not hours)
  • Deterministic (no flakes)
  • Readable (acts as executable documentation)
  • Resilient (survives refactors)
  • High signal (fails for meaningful reasons)

1) Mental model: what a good unit test is

A unit test verifies a small unit of behavior in isolation.

At scale, โ€œunitโ€ usually means:

  • A pure function
  • A method with minimal dependencies
  • A domain service with dependencies replaced by fakes/stubs

Not unit tests: tests that hit the network, filesystem, database, clock, random generator, static singletons, or global state.


2) The principle: test behavior, not implementation

Behavior: what the component guarantees to consumers (contract). Implementation: how it currently does it.

Prefer assertions like:

  • result values
  • emitted domain events
  • commands sent to collaborators
  • published messages

Avoid asserting:

  • exact internal method call sequences unless that is the contract
  • private state
  • exact query strings

Refactor resilience heuristic

If you can refactor the code (rename methods, change internal algorithm) without changing the externally visible behavior, then unit tests should not need to change.


3) Boundaries and seams (how you get testability)

A maintainable unit-test strategy comes from architecture:

  • Domain layer: mostly pure logic; easiest to unit test
  • Application layer: orchestrates use cases; unit test with fakes
  • Infrastructure: prefer integration tests

Design-for-test rules

  • Put I/O behind interfaces.
  • Inject time (IClock), randomness (IRandom), IDs (IIdGenerator).
  • Avoid static state; prefer DI.
  • Keep constructors cheap (no heavy work).

4) Patterns that scale

AAA (Arrange/Act/Assert)

Keep each section small and explicit.

Given/When/Then (BDD style)

Same as AAA, optimized for readability.

Test data builders

Create readable object graphs.

public sealed class OrderBuilder
{
    private Guid _id = Guid.NewGuid();
    private List<OrderLine> _lines = new();

    public OrderBuilder WithId(Guid id) { _id = id; return this; }
    public OrderBuilder WithLine(string sku, int qty)
    {
        _lines.Add(new OrderLine(sku, qty));
        return this;
    }

    public Order Build() => new Order(_id, _lines);
}

Parameterized tests

Use for input matrices.

[Theory]
[InlineData(0, false)]
[InlineData(1, true)]
public void IsValidQuantity(int qty, bool expected)
{
    Assert.Equal(expected, OrderRules.IsValidQuantity(qty));
}

5) Common test smells (and what to do instead)

  • Brittle tests: assert on internal details โ†’ assert on outcomes/contract.
  • Mystery guests: hidden setup in shared fixtures โ†’ make setup explicit or use builders.
  • Assertion roulette: many asserts with no message โ†’ group asserts, use descriptive names.
  • Slow tests: heavy object graphs or real dependencies โ†’ mock/fake, move to integration tests.
  • Over-mocking: tests mirror implementation โ†’ use state-based testing or higher-level tests.

6) Mocking policy (unit tests)

At scale, you need a clear policy:

  • Mock outgoing dependencies (DB, queue, HTTP, filesystem)
  • Prefer fakes over mocks for rich dependencies (in-memory repositories)
  • Prefer state-based assertions over interaction-based assertions

Example: fake repository + state-based test

public sealed class InMemoryUserRepo : IUserRepository
{
    private readonly Dictionary<Guid, User> _users = new();
    public Task AddAsync(User user) { _users[user.Id] = user; return Task.CompletedTask; }
    public Task<User?> FindAsync(Guid id) => Task.FromResult(_users.TryGetValue(id, out var u) ? u : null);
}

[Fact]
public async Task Register_creates_user_and_returns_id()
{
    var repo = new InMemoryUserRepo();
    var service = new RegistrationService(repo);

    var id = await service.RegisterAsync("a@b.com");

    var saved = await repo.FindAsync(id);
    Assert.NotNull(saved);
    Assert.Equal("a@b.com", saved!.Email);
}

7) Flake-proofing (critical for leads)

Unit tests must be deterministic:

  • Never use DateTime.Now directly โ†’ inject an IClock.
  • Never use randomness without controlling seed.
  • Avoid parallel shared mutable state.
  • Avoid real timers.

Clock injection example

public interface IClock { DateTimeOffset UtcNow { get; } }
public sealed class SystemClock : IClock { public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; }
public sealed class FakeClock : IClock { public DateTimeOffset UtcNow { get; set; } }

8) Coverage: how to think about it

Coverage is a metric, not a goal.

Good targets depend on domain criticality:

  • Domain logic: aim high (70โ€“90%+), but focus on critical invariants
  • Controllers/infrastructure: lower coverage, more integration tests

Prefer measuring:

  • defect escape rate
  • time-to-fix
  • flaky rate
  • runtime of suite

9) Interview angle (Lead-level)

Be ready to answer:

  • โ€œWhat makes a good unit test?โ€
  • โ€œHow do you prevent flaky tests?โ€
  • โ€œWhatโ€™s your mocking strategy and why?โ€
  • โ€œHow do you balance unit vs integration tests?โ€

Strong TL answer includes:

  • a test pyramid strategy
  • deterministic rules (clock/random/I/O)
  • patterns (builders, parameterized tests)
  • a policy for what gets unit-tested

10) Review checklist (for real projects)

  • [ ] Unit test suite runs in minutes.
  • [ ] No external I/O in unit tests.
  • [ ] Clock/randomness are injected.
  • [ ] Tests assert on behavior/contract.
  • [ ] Builders/factories exist for complex setup.
  • [ ] Flaky tests are treated as incidents (fix or quarantine quickly).
  • [ ] CI enforces fast feedback (fail fast, parallelize, split slow suites).

๐Ÿ“š Related Articles