DOTNET Contents

Unit Testing with xUnit (Basics)

xUnit unit tests are not about “green checkmarks”. They are a safety net for pure logic and edge cases that will absolutely blow up in prod: time boundaries, nullability, parsing, and invariants. Keep them fast, deterministic, and ruthless.

On this page

Production incident

A pricing bug ships on Friday. A boundary condition (end-of-month + timezone conversion) flips a discount rule. Unit tests existed, but they were “happy path” only and used DateTime.Now, so they were non-deterministic and never covered the edge case. Support sees wrong invoices. Engineers scramble to hotfix. This was preventable: unit tests should lock down invariants and edge cases, not just execute code.

Symptoms

  • Small code change causes unexpected business logic regression.
  • Bugs show up only in certain time windows (DST, month end) or inputs (empty strings, nulls, out-of-range).
  • Tests are flaky locally and in CI.

Root causes

  • Tests depend on wall clock (DateTime.Now) and real randomness.
  • Too many integration concerns in unit tests (DB, HTTP, filesystem).
  • No boundary testing: no null/empty/out-of-range coverage.
  • Tests assert implementation details rather than behavior/invariants.

Diagnosis

# Find flaky patterns
grep -R "DateTime\.Now\|DateTime\.UtcNow\|Random\(" -n test
grep -R "Thread\.Sleep\|Task\.Delay" -n test
grep -R "HttpClient\|DbContext\|SqlConnection" -n test

Anti-pattern

[Fact]
public void Discount_is_applied()
{
    var svc = new PricingService();
    var price = svc.Calculate(100, DateTime.Now); // non-deterministic
    Assert.True(price < 100);
}

This test will be flaky, and it doesn't lock down a real rule.

Correct pattern

  • Unit tests must be fast (milliseconds), deterministic, and pure.
  • Inject time and randomness behind interfaces.
  • Test boundaries and invariants: “never negative”, “never exceeds max”, “idempotent for same input”.

Deterministic time injection

public interface IClock { DateTime UtcNow { get; } }

public sealed class FakeClock : IClock
{
    public DateTime UtcNow { get; set; }
}

public sealed class PricingService
{
    private readonly IClock _clock;
    public PricingService(IClock clock) => _clock = clock;

    public decimal Calculate(decimal amount) => amount; // simplified
}

public class PricingTests
{
    [Fact]
    public void Calculate_is_deterministic()
    {
        var clock = new FakeClock { UtcNow = new DateTime(2026, 2, 28, 0, 0, 0, DateTimeKind.Utc) };
        var svc = new PricingService(clock);

        var result = svc.Calculate(100);

        Assert.Equal(100, result);
    }
}

Boundary testing mindset

  • Null/empty/whitespace inputs.
  • Minimum/maximum values.
  • Parsing failures and format variations.
  • Time boundaries: midnight UTC, DST transitions (if you must deal with local time), month-end.

Security and performance impact

  • Security: unit tests can enforce input validation invariants (reject invalid IDs, length limits).
  • Performance: keep unit tests fast. Slow unit tests kill iteration speed and people stop running them.

Operational notes

  • Rollout: require unit tests for bugfixes. Every postmortem should add at least one test that would have caught it.
  • CI: unit tests run on every PR, no exceptions. Flaky tests are treated as broken code.

Checklist

  • Unit tests are deterministic (no wall clock, no real randomness, no sleeps).
  • They test behavior/invariants, not private implementation details.
  • Boundary cases are covered.
  • Run time is fast enough for every PR.
  • Flaky tests are fixed or deleted immediately.