Unit Testing with xUnit (Basics)
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.