Test Data and CI
A test suite earns its keep in two ways: developers trust it (it fails only when something is really broken), and it runs automatically where merges happen. This lesson covers both β the craft of test data, and wiring the suite into Azure DevOps.
Builders and fixtures: taming test data
Domain objects in this system are deep: a candidate has transcripts, transcripts have courses, courses map to SMR domains. Constructing that inline makes every test forty lines of noise β and when a required field is added, a hundred tests break at once. The builder pattern fixes both:
var candidate = new CandidateBuilder()
.WithCourse("MATH 210", units: 4.5m)
.WithExamScore("CSET Math II", passed: true)
.Build();
The builder supplies valid defaults for everything, and each test overrides only what it's
actually about β so the test reads as its own specification, and schema changes are absorbed
in one place. Fixtures complement builders for shared expensive setup: xUnit's
IClassFixture<T> (or NUnit's [OneTimeSetUp]) builds a database or seeded context once per
class instead of once per test. Use fixtures for infrastructure, builders for data β and keep
fixture state read-only, or tests start coupling through it.
Deterministic tests, or nothing
A flaky test β passes locally, fails in the pipeline, passes on re-run β is worse than no test: the team learns to click "re-run" and real failures die in the noise. Flakiness has a short list of causes, all avoidable:
DateTime.Now. The classic. Logic like "exam score expired?" must take a clock abstraction (IClock, or aDateTimeparameter) so tests can pin today. A test that passes except during year-end is not a test.- Shared mutable state β static caches, a common database record two tests both edit. Isolate per test; parallel runners will find what you missed.
- Ordering assumptions. SQL without
ORDER BYhas no order; assert on sets, or sort first. - Real time and real networks. No
Thread.Sleep, no live HTTP in unit tests.
House rule worth adopting: a flaky test gets fixed or deleted the week it's detected β never quarantined indefinitely.
Running in Azure DevOps
In an Azure DevOps pipeline, tests are a YAML step away β dotnet test (or the
VSTest@2 task for Framework projects) with --logger trx so results publish to the run
summary, giving reviewers pass/fail history and failure details per build. The policies that
make it matter: run tests on every pull request via a branch policy (a red suite
blocks the merge), keep the whole job fast enough that nobody is tempted to skip it, and treat
a broken main-branch build as the team's top priority.
Coverage: a signal, not a target
--collect "XPlat Code Coverage" adds a coverage report to the pipeline. Read it as a
flashlight: uncovered evaluation-rule code is a genuine gap worth closing. Never make the
number a goal β chasing "80%" produces assertion-free tests over trivial code (Goodhart's
law), while your riskiest branch sits uncovered. Coverage tells you where you haven't
tested; only test quality tells you whether you've tested well.