CI/CD / Integration and candidate foundations / Lesson 5
Tests, exit status, and useful release gates
Make checks meaningful by connecting assertions, status propagation, and retained evidence.
On this page
Lesson goals
- Explain the difference between executing tests and enforcing their result.
- Design fast and slower checks around distinct risks.
A gate should address a named risk
A release gate is useful when failure predicts a risk that matters and someone can act on it. Parcelโs unit test checks address-label normalization; a contract test checks the shape expected by a carrier adapter; a smoke check asks whether the packaged entry point can start in a suitable lab. These tests answer different questions. Counting tests or demanding a coverage percentage alone does not establish correct behavior. Identify the failure mode, the evidence produced, and the release decision that evidence controls.
Use examples that expose the contract
A normalizer that trims surrounding whitespace and uppercases a postal code has simple deterministic behavior. The following complete example tests both transformations and an empty input. It does not validate real postal formats or international address rules, so the test name should not imply those capabilities. Assertions should fail for the intended defect, be easy to diagnose, and avoid dependence on wall-clock time or external availability unless those dependencies are the actual subject of the check.
def normalize(code):
return code.strip().upper()
cases = [(" ab12 ", "AB12"), ("", ""), (" x9", "X9")]
for supplied, expected in cases:
assert normalize(supplied) == expected, (supplied, expected)
print("3 normalization cases passed")
3 normalization cases passed
All inputs are synthetic strings. The script exits successfully only if all assertions hold. It makes no claim about legal postal-code validity.
Preserve failure through the wrapper
A correct test can be neutralized by a wrapper that ignores its exit status. Shell pipelines, continue-on-error settings, and unconditional success commands can make a failed suite appear green. Distinguish test execution, report publication, and gate evaluation. A report collector should preserve evidence even after failure, but its own success must not replace the suite result. In shell environments, inspect the selected shellโs pipeline and error semantics rather than assuming every runner behaves like a local interactive terminal.
python test_normalizer.py
result=$?
# A report publisher would run here under its own reviewed policy.
exit "$result"
This fragment assumes a shell that continues after the test command and a test_normalizer.py file. It is not a complete workflow. If the platform enables immediate exit, use its explicit always-run report mechanism and keep the original test status as the required gate.
Arrange checks by cost and diagnostic value
Fast deterministic tests usually belong early because they give inexpensive feedback. Integration and compatibility checks need controlled fixtures and explicit cleanup; end-to-end tests cover important journeys but may diagnose poorly when too much changes at once. Do not ban a slow test merely because it is slow. Ask whether its risk can be covered earlier, whether it can be isolated, and whether the remaining expensive check belongs on every proposal, the integration path, or a periodic compatibility path. Required release evidence still needs a defined freshness rule.
Treat unstable tests as a defect
A test that alternates between pass and fail on unchanged inputs erodes trust. Retrying once may collect evidence about instability, but repeatedly rerunning until green hides the failure rate. Record attempt outcomes separately, capture the seed or timing assumptions, and assign an owner. A temporary quarantine needs an expiry and replacement coverage for the risk. Never silently convert a mandatory check into optional reporting. Investigate whether shared state, ordering, external services, or an actual race explains the variation before selecting a remedy.
Common errors and fixes
The test log contains failures but the required job is green.
- Why it happens
- A wrapper swallowed the failing exit code or ignored errors.
- How to fix it
- Make the original suite status authoritative and upload reports separately.
- Prevent it next time
- Review failure propagation with a deliberately failing synthetic case.
A retry policy hides a frequently failing test.
- Why it happens
- Only the final successful attempt is reported.
- How to fix it
- Retain all attempts and investigate nondeterministic inputs.
- Prevent it next time
- Track instability and time-limit any quarantine.
High coverage is treated as proof of correct labels.
- Why it happens
- Executed lines were confused with asserted behavior.
- How to fix it
- Add risk-focused examples and meaningful assertions for edge cases.
- Prevent it next time
- Review what each test would detect and what remains outside scope.
Check your understanding
1. A suite passes after its fourth retry. What should the release record contain?
Show worked solution
Record the failed and successful attempts, whether retry acceptance is allowed by policy, and the instability owner. A final pass alone loses important evidence. If the failure concerns a release-critical race, a retry does not establish that the risk disappeared.
2. A contract test needs a carrier service unavailable to proposals. How can the process retain useful coverage?
Show worked solution
Use a controlled local contract fixture for proposal checks, validate that fixture against a separately authorized compatibility path, and require appropriately fresh compatibility evidence before release. State the limitation rather than granting proposal code privileged external access.
Key takeaways
- A test gate needs a named risk and preserved failure status.
- Reports explain outcomes; they do not replace test outcomes.