Software QA / Basic: reasoning and test design / Lesson 3
Test levels, isolation, and useful assertions
Place checks where they explain failures clearly, while preserving evidence that components cooperate. Use a cancellation refund to compare unit, integration, system, and acceptance testing.
On this page
Lesson goals
- Select test levels for different risks.
- Distinguish doubles from real integration evidence.
- Assert outcomes and relevant side effects.
Choose a level by the question
A unit test can ask whether a refund policy returns the right number of cents; an integration test can ask whether the refund record and balance update share a transaction. A system test exercises the assembled product through a supported interface. Acceptance testing checks suitability against stakeholder needs. These are purposes and scopes, not rigid tool names. One browser script does not automatically represent acceptance, and one database test does not automatically cover every integration risk. Select the smallest practical scope that can expose the target failure accurately.
Worked case: policy arithmetic
The agreed policy refunds the full fee when cancellation is at least 48 hours before a workshop and half otherwise. Fees use integer cents; this example assumes even values so rounding is outside scope. Check exactly 48 hours and just below it. These cases target the comparison operator and avoid unrelated network setup. A good assertion compares the returned amount with a separately derived value. If the policy changes, the test should fail with a clear business disagreement rather than a vague object mismatch.
def refund_cents(fee, hours_before):
return fee if hours_before >= 48 else fee // 2
assert refund_cents(12000, 48) == 12000
assert refund_cents(12000, 47) == 6000
The first assertion catches a mistaken strict greater-than rule. The second checks the lower partition. Neither assertion checks a payment provider.
Doubles answer bounded questions
A stub can return a provider timeout; a fake may implement a small in-memory ledger; a mock may check that a call was requested. Each substitute simplifies the environment, but it can encode a mistaken understanding of the real dependency. Keep some integration evidence against a controlled real component or agreed contract. Avoid asserting a long internal call sequence when only the final refund matters. Such assertions make harmless refactoring expensive and may still miss a wrong amount or duplicate refund visible to the learner.
Worked case: failure between writes
Create one paid booking and a zero-refund ledger in an isolated database. Trigger a cancellation while a controlled dependency fails after the refund record is prepared. The agreed atomic behavior is that neither the refund nor the balance change is committed. On a subsequent successful attempt, expect exactly one refund and the correct balance. This checks a boundary between components that the arithmetic unit test cannot reach. Record the injection point; failing before any write would not demonstrate rollback of an already-started transaction.
Compose evidence without wasteful duplication
A balanced suite may have many cheap policy cases, a smaller set of transaction and provider-contract cases, and a few complete user journeys. The exact shape follows risk and architecture rather than a universal ratio. Preserve a system journey confirming that a learner can cancel and later see the refund status; it checks wiring and presentation missing from lower levels. When it fails, use focused checks to diagnose the layer. Keep the distinction between confirmation testing of a fix and regression testing of potentially affected behavior.
Common errors and fixes
Every rule is tested through the browser
- Why it happens
- The team confuses realism with diagnostic value.
- How to fix it
- Move pure policy combinations to focused tests and retain representative journeys.
- Prevent it next time
- Choose a scope based on the risk being investigated.
A fake provider becomes the only evidence
- Why it happens
- The substitute and application share a mistaken contract.
- How to fix it
- Add controlled contract or real-component integration checks.
- Prevent it next time
- Review doubles when dependencies change.
Checking a call but ignoring its outcome
- Why it happens
- Implementation activity substitutes for user-visible correctness.
- How to fix it
- Assert amount, persisted state, and duplicate prevention.
- Prevent it next time
- Ask which harmful result each assertion detects.
Check your understanding
1. Where should you test ten discount combinations and one payment timeout?
Show worked solution
Test deterministic discount combinations at the policy unit level with independent expected values. Test timeout handling at a service boundary using a controlled double, then preserve separate contract evidence for the real provider. A complete journey checks that the timeout message and retry behavior reach the user.
2. A cancellation test asserts only that the refund function was called. Improve it.
Show worked solution
For a 12,000-cent booking canceled 47 hours before the workshop, expect 6,000 cents, one refund record, and the agreed status. Repeat the same cancellation and expect no second financial effect. These observations expose wrong amounts and duplicate handling that a call assertion misses.
Key takeaways
- Use lower levels for precise rules and higher levels for cooperation and journeys.
- A test double simplifies a dependency; it does not validate the real contract.