Deliverable
Test case suite — login and payment
Fourteen cases from the same fictional billing app, in the shape I write them: one behavior per case, preconditions stated, expected results specific enough to fail on. Twelve are automated in the demo repo; two stay manual on purpose.
Sample deliverable. Written for this site against a fictional product with no client data and no confidential material.
How I write these
- One behavior per case. A case that tests three things fails ambiguously.
- Preconditions stated, so the case is runnable by someone who did not write it.
- Expected results specific enough to fail on. "Works correctly" is not an expected result.
- Priority reflects business risk, not implementation difficulty.
- Negative and boundary cases carry equal weight to happy paths — that is where the defects are.
Cases
| ID | Case | Precondition | Steps | Expected result | Pri | Automated in |
|---|---|---|---|---|---|---|
| TC-AUTH-01 | Sign in with valid credentials | Active account, verified email | Enter registered email and correct password. Submit. | Redirect to dashboard. Session cookie set with Secure and HttpOnly flags. | P1 | test_auth.py |
| TC-AUTH-02 | Sign in with wrong password | Active account | Enter registered email and an incorrect password. Submit. | Generic failure message. Response must not reveal whether the email exists. No session created. | P1 | test_auth.py |
| TC-AUTH-03 | Lockout after repeated failures | Active account, lockout threshold set to 5 | Submit an incorrect password five consecutive times. | Sixth attempt is rejected regardless of password correctness. Lockout notice sent to the account email. | P1 | test_auth.py |
| TC-AUTH-04 | Session expiry during an idle period | Signed in, idle timeout 30 minutes | Sign in, leave the tab idle past the timeout, then trigger an authenticated action. | Action rejected, user returned to sign-in with the destination preserved. No unhandled error. | P2 | Manual |
| TC-PAY-01 | First charge with a valid card | New account, no stored payment method, sandbox processor | Select the monthly plan, enter a valid test card, confirm. | Charge succeeds. Subscription active. Receipt email sent. Ledger webhook delivered exactly once. | P1 | test_checkout.py, test_billing_db.py |
| TC-PAY-02 | Declined card at first charge | New account, sandbox decline card | Select the monthly plan, enter the decline test card, confirm. | Clear decline message with the processor reason. No subscription created. No partial account state left behind. | P1 | test_checkout.py |
| TC-PAY-03 | Double submission of the payment form | New account, valid test card | Submit the payment form, then submit again before the first response returns. | Exactly one charge. Second submission is idempotent, not a second authorization. | P1 | test_checkout.py, test_billing_db.py |
| TC-PAY-04 | Card expiring before the next renewal | Active subscription, stored card expiring within the billing period | Advance the clock to the renewal date in the time-shifted environment. | Renewal attempt fails cleanly, dunning sequence starts, account not suspended before the retry window closes. | P2 | Manual |
| TC-PAY-05 | Card expiry in the past is rejected before submission | New account, valid test card number | Enter a valid test card number and an expiry date in the past, for example 01/22. Confirm. | Inline error shown for the expiry. No request is sent to the payment API. | P2 | test_checkout.py |
| TC-PLAN-01 | Upgrade mid-cycle with proration | Active monthly subscription, 12 days into a 30-day period | Change to the higher-tier plan and confirm. | Prorated charge matches the finance-supplied expected value to the cent. New tier effective immediately. | P1 | test_plans.py |
| TC-PLAN-02 | Downgrade defers to period end | Active higher-tier subscription | Change to the lower-tier plan and confirm. | No immediate charge or refund. Current tier retained until period end. Scheduled change recorded with its effective date. | P2 | test_plans.py |
| TC-PLAN-03 | Upgrade mid-cycle in a 31-day month | Active monthly subscription, 12 days into a 31-day period | Change to the higher-tier plan and confirm. | Prorated charge uses the real period length and matches the finance-supplied expected value to the cent. | P1 | test_plans.py |
| TC-HOOK-01 | Duplicate webhook delivery | Ledger consumer running | Replay an identical payment-succeeded event twice. | Ledger records one entry. Duplicate acknowledged, not reprocessed. | P1 | test_webhooks.py |
| TC-HOOK-02 | Malformed webhook payload | Ledger consumer running | Deliver an event with a missing required field. | Rejected with a 4xx and logged with the event ID. No partial write. No retry storm. | P2 | test_webhooks.py |
Automation
Each automated test names the case it covers, so a failing test points straight back at the manual case and its expected result. The two manual cases need the time-shifted environment, and nothing gets automated until it has run manually twice and stopped changing.
@pytest.mark.regression
@pytest.mark.known_defect("BUG-1156")
@pytest.mark.tc("TC-PLAN-03")
def test_upgrade_mid_cycle_prorates_a_31_day_period(api, token, clock):
...Three of the cases fail on the release-candidate build because they reproduce the defects in the demo app: TC-PAY-03 (BUG-1142), TC-PLAN-03 (BUG-1156) and TC-PAY-05 (BUG-1163).
Import format
The same suite exports to Xray or Zephyr CSV without rework, because the columns above map directly onto their import fields. The full library is in the demo repository as docs/test-cases.csv.
Test Case ID,Summary,Precondition,Test Steps,Expected Result,Priority TC-AUTH-02,Sign in with wrong password,Active account,"Enter registered email and an incorrect password. Submit.","Generic failure message. Response must not reveal whether the email exists. No session created.",P1