Tests and Quality Gates: A Report Is Not a Stop Signal
Build a layered test strategy, publish usable results, enforce failures, and use Copilot or MCP without trusting generated coverage claims.
A dashboard warning is not a locked door
A smoke alarm reports a problem; a fire door limits what can happen next. A test report and a release gate differ in the same way. A pipeline can upload a red test report and still deploy if no task result, condition, or approval actually blocks it.
Define quality policy first: which failures block a PR, which block deployment, who may approve exceptions, and what evidence is retained. Then implement the enforcement and test it with a controlled failure.
Put each test where it gives useful feedback
| Layer | Example for a booking service | Placement |
|---|---|---|
| Local checks | Formatting, syntax, focused developer tests | Fast feedback before push |
| Unit | Retry decision returns the expected value | Every change |
| Integration | API and queue produce one confirmation | CI with controlled dependencies |
| End-to-end | User can complete a booking | Test environment and selected release checks |
| Load | Latency/error criteria under expected traffic | Planned controlled performance run |
| Operational smoke | Deployed version answers a safe health request | After deployment |
Avoid βmore tests everywhereβ as a strategy. Prefer tests tied to risks. Unit tests do not prove a real identity can reach a database. Load tests do not prove a bookingβs business result is correct.
Load runs can incur charges and disrupt services. Use an approved test environment, define duration and concurrency, monitor dependencies, and set stop criteria. Never aim an exploratory load test at someone elseβs endpoint.
Publish results and enforce the outcome separately
This Azure Pipelines example assumes the projectβs test command produces JUnit files under test-results:
steps:
- script: npm ci
displayName: Restore
- script: npm test
displayName: Run tests
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testResultsFormat: JUnit
testResultsFiles: 'test-results/*.xml'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
The test command should exit nonzero on failures. Publishing with succeededOrFailed() preserves evidence after a failed test command; it does not convert failure into success. The two failure flags address different risks: reported failing tests and no report matching the expected path.
Do not use continueOnError or || true merely to make a quality gate green. If your framework uses another report format, configure the appropriate publisher and path. A hosted agent must have the needed runtime; integration tests also need authorized connectivity and isolated test data.
GitHub Actions has the same separation: run the test command, upload diagnostics with an appropriate condition such as always(), and require the test jobβs check on the PR. Artifact upload success is not test success.
Coverage is a map, not a proof
Line coverage says which instrumented lines ran, not whether assertions were meaningful. Branch coverage can expose untested decisions, but neither establishes that all requirements were tested.
Collect coverage with the projectβs test framework, publish supported coverage output, and enforce an agreed threshold in a tool that actually returns a failure. Azureβs coverage publishing task makes a report visible; do not assume that publishing alone applies your desired threshold.
For a change to retry logic, inspect the uncovered error and repeated-request paths. A high repository-wide percentage can hide the one path the customer depends on.
Use Copilot and MCP as an evidence assistant
Give an approved assistant the relevant function, acceptance criteria, and existing tests. Ask for focused edge cases, not a large generated suite:
Propose tests for duplicate request identifiers and transient failure.
Use the existing test framework. Show why each assertion would fail
without the fix. Do not change the production behavior or gate policy.
Review tests for assertions that merely repeat the implementation. Run them before and after the fix in an isolated branch where appropriate.
With authorized MCP access, ask for a specific pipeline runβs failed test summary and coverage artifacts. Require the exact run/commit and source links. Let the assistant propose a YAML change, but inspect the diff for suppressed failures, broadened permissions, missing-result handling, and accidental deployment triggers.
If tools cannot read the report, say so. A fluent invented pass percentage is worse than a clearly missing result.
The test publisher succeeds, but no report exists at its configured path. Which control makes this absence fail explicitly in PublishTestResults@2?
Try it: negative-control the gate
Prerequisites: a sandbox repository with a small test runner and report output; a sandbox pipeline if testing the publisher. CI usage can be billed.
Run three controlled cases: one passing test, one failing assertion, and an intentionally wrong report path. Inspect the command exit, publisher result, visible report, and merge/deploy condition for each. Restore the correct path afterwards. Ask an assistant to explain the evidence without changing policy.
Expected observation: the failed assertion and required missing report block the intended path. Diagnostics remain accessible. βNo tests foundβ is not silently treated as a measured pass.
Cleanup: remove disposable failing tests and restore the sandbox workflow. Do not weaken shared thresholds to complete this exercise.
Decision check: a flaky test blocks delivery
Capture its repeated outcomes and ownership. Quarantine can be a time-bounded, visible exception with replacement coverage; silently ignoring failures turns an unreliable signal into no signal.