How to Write Test Cases for Promo Codes (With Examples)
How to Write Test Cases for Promo Codes (With Examples)
How to Write Test Cases for Promo Codes (With Examples)
Promo codes are a common feature in e‑commerce, SaaS, and mobile apps that directly affect revenue, user acquisition, and conversion metrics. Because they touch pricing logic, validation rules, and often integrate with payment gateways, a defect in promo‑code handling can lead to financial loss, compliance issues, or poor user experience. Writing effective test cases for promo codes therefore requires a disciplined approach that combines clear requirement traceability, systematic positive/negative/edge/boundary coverage, and a plan for both manual execution and automated regression. This guide walks you through the full lifecycle—from dissecting the feature spec to building a reusable test matrix, setting up test data, prioritizing effort, and pairing your cases with autonomous exploration for real‑world confidence.
How to Write Test Cases for Promo Codes (With Examples): Test‑Case Anatomy
A well‑structured test case makes it easy to review, maintain, and automate. Each case should contain the following fields:
| Field | Purpose | Example Content | |
|---|---|---|---|
| ID | Unique identifier for traceability | TC‑PROMO‑001 | |
| Title | Short, readable summary | Verify that a valid 10 % discount code reduces order total correctly | |
| Preconditions | State that must exist before steps are executed | User is logged in, cart contains two items worth $100, promo‑code service is reachable | |
| Steps | Ordered actions the tester or automation performs | script performs | 1. Navigate to checkout page. 2. Enter promo code “SAVE10” in the discount field. 3. Click “Apply”. 4. Observe order summary. |
| Expected Result | Observable outcome that determines pass/fail | Order total shows $90 (10 % off $100) and a success message “Promo code applied”. | |
| Test Data | Specific values used in steps (can be referenced from a data sheet) | Promo code = SAVE10, discount type = percentage, value = 10 | |
| Postconditions | State the system should be left in (optional) | Promo code marked as used once for this user session. | |
| Automation Notes | Hints for turning the case into a script | Use CSS selector #promo-input for code entry; assert on .order-total text. |
When you define a template like the table above, you create a contract that reviewers can check off quickly. Keep the wording imperative and avoid ambiguous language such as “should maybe” or “might”. Every step must be testable with a clear pass/fail criterion.
How to Write Test Cases for Promo Codes (With Examples): Building Positive, Negative, Edge, Boundary Cases
Promo‑code logic typically involves validation (format, eligibility, usage limits), calculation (discount type, stacking rules), and presentation (UI feedback). To achieve high signal, you need to cover four categories:
Positive Cases
These verify that the system behaves correctly when all conditions are satisfied.
- Valid code, eligible user, first‑time use – the core happy path.
- Valid code, eligible user, not yet exhausted – ensures usage‑limit counters work.
- Valid code, eligible user, applies to specific product/category – checks product‑scope rules.
- Valid code, stackable with another promotion – validates combination logic if allowed.
- Valid code, applies free shipping – confirms non‑monetary benefits.
Negative Cases
These confirm that the system rejects invalid input or ineligible scenarios.
- Malformed code (wrong length, illegal characters) – e.g., “ABC!@#”.
- Expired code – code whose validity date is in the past.
- Future‑dated code – code not yet active.
- Code used beyond allowed limit – e.g., a single‑use code applied twice.
- Code not applicable to cart items – e.g., a code for brand X when cart contains brand Y.
- User not meeting eligibility (new‑user‑only code used by existing user) – checks user‑segment rules.
- Code disabled/admin‑revoked – verifies that a flag in the admin panel blocks usage.
- Attempt to apply code after payment – ensures the discount field is read‑only post‑transaction.
Edge Cases
These sit at the boundaries of input domains or business rules.
- Code with maximum allowed length – if the system accepts up to 20 characters, test a 20‑char code.
- Code with minimum allowed length – test a 1‑character code if permitted.
- Code containing leading/trailing spaces – ensures trim logic works.
- Code case‑sensitivity – if the system treats codes case‑insensitively, test “SaVe10” vs “save10”.
- Zero‑value discount – a code that offers 0 % off or $0 off; should still be accepted but not change total.
- 100 % discount – code that makes the order free; verify that tax and shipping are handled correctly per policy.
- Negative discount value – if the backend mistakenly allows a negative number, ensure it is rejected or treated as a surcharge.
- Maximum cart value – apply a code to a cart that hits the system’s maximum order amount (e.g., $10 000) to check for overflow issues.
- Minimum cart value – apply a code to a cart just above the minimum threshold for eligibility.
Boundary Cases
These focus on numeric limits and temporal boundaries.
- Discount percentage boundaries – test 0.01 %, 99.99 %, 100 % if allowed.
- Monetary discount boundaries – test $0.01, $9999.99, etc.
- Usage‑limit boundaries – if a code can be used 500 times, test the 500th and 501st attempt.
- Date‑time boundaries – test a code that expires at 23:59:59 on a given day; try one second before and one second after.
- Time‑zone handling – if the server uses UTC but the frontend shows local time, verify that a user in GMT+5 sees the correct active window.
- Leap year / daylight‑saving transitions – ensure date logic works on Feb 29 and during clock shifts.
By enumerating cases across these four quadrants, you create a matrix that catches the majority of logical faults while keeping the test set maintainable.
How to Write Test Cases for Promo Codes (With Examples): Data Setup and Test Environment Preparation
Reliable promo‑code testing hinges on controlled data. Unlike UI‑only tests, promo‑code validation often reads from a database, calls a microservice, or checks cached rules. Follow these steps to prepare a repeatable environment:
- Isolate the promo‑code service – Deploy a dedicated instance or use a feature flag that points tests to a stub/mock. This prevents interference from other test suites.
- Load reference data sets – Create CSV or JSON fixtures that contain:
- Promo‑code records (code string, discount type, value, start/end dates, usage limit, eligibility flags, product scopes).
- User‑segment records (new user, returning user, VIP, geo‑location).
- Product‑catalog entries with prices, tax codes, and inventory.
- Use database transactions or snapshots – Wrap each test in a transaction that rolls back after execution, or revert to a known snapshot via Docker volumes or VM snapshots. This guarantees that usage‑limit counters start at zero for each test.
- Mock external gateways – If the promo‑code service calls a payment provider or a tax engine, replace those calls with mocks that return deterministic responses. This isolates the discount logic from external variability.
- Parameterize environment variables – Store endpoints, API keys, and feature flags outside the test code (e.g., in a
.envfile) so the same test suite can run against dev, staging, and production‑like environments without modification. - Automate data cleanup – Implement a teardown hook that deletes any test‑generated orders, usage logs, or temporary coupons to keep the database lean.
A concrete example using pytest and fixtures in Python:
import pytest
from myapp.models import PromoCode, User, Order
from myapp.services import PromoService
@pytest.fixture
def promo_code():
code = PromoCode.objects.create(
code="TEST10",
discount_type="percent",
value=10,
starts_at="2025-01-01T00:00:00Z",
ends_at="2025-12-31T23:59:59Z",
usage_limit=1,
applicable_to="all",
)
yield code
# teardown
code.delete()
@pytest.fixture
def user():
u = User.objects.create(email="tester@example.com", is_new=False)
yield u
u.delete()
def test_valid_promo_applies(promo_code, user):
service = PromoService()
order = Order.create(user=user, total_cents=10000) # $100.00
result = service.apply_promo(order, promo_code.code)
assert result.success
assert order.total_cents == 9000 # $90.00 after discount
The fixture guarantees a fresh promo‑code record for each test, and the rollback (implicit in the fixture’s yield) cleans up after the test. Similar patterns exist in JUnit (@BeforeEach, @AfterEach), TestNG, or using Docker Compose to spin up a disposable database.
How to Write Test Cases for Promo Codes (With Examples): Prioritization and Traceability to Requirements
Not all test cases carry equal risk. Use a risk‑based prioritization scheme (e.g., P0‑P3) to allocate effort where a failure would hurt the business most.
| Priority | Definition | Typical Promo‑Code Scenarios |
|---|---|---|
| P0 | Show‑stopper; would cause revenue loss, legal violation, or severe UX breakdown. | • Applying a valid code results in no discount or incorrect total. • Code can be applied unlimited times despite a usage limit. • Expired code still reduces price. |
| P1 | High impact; affects conversion funnels or key user segments. | • New‑user‑only code works for existing users. • Stacking rules produce unexpected totals. • Free‑shipping code does not remove shipping fee. |
| P2 | Medium impact; edge‑case or cosmetic issues. | • Leading/trailing spaces in code are not trimmed. • Case‑sensitivity mismatch. • Success message wording typo. |
| P3 | Low impact; nice‑to‑have or rarely used paths. | • Code with maximum length (20 chars) works. • Informational tooltip appears on hover. |
To trace each test case back to a requirement, maintain a simple matrix:
| Requirement ID | Description | Linked Test‑Case IDs |
|---|---|---|
| REQ‑PROMO‑001 | Promo code must reduce order total by the specified percentage or amount. | TC‑PROMO‑001, TC‑PROMO‑002, TC‑PROMO‑003 |
| REQ‑PROMO‑002 | Code must be rejected if expired or not yet active. | TC‑PROMO‑010, TC‑PROMO‑011 |
| REQ‑PROMO‑003 | Usage limit per code must be enforced. | TC‑PROMO‑015, TC‑PROMO‑016 |
| REQ‑PROMO‑004 | Code eligibility based on user segment (new‑user, VIP, geo). | TC‑PROMO‑020, TC‑PROMO‑021 |
| REQ‑PROMO‑005 | Discount must not be applicable to excluded product categories. | TC‑PROMO‑025, TC‑PROMO‑026 |
| REQ‑PROMO‑006 | System must display appropriate error messages for invalid input. | TC‑PROMO‑030 … TC‑PROMO‑035 |
When a requirement changes, you can instantly see which test cases need review or creation. This traceability also satisfies audit needs for regulated industries (e.g., finance, healthcare) where promo‑code discounts might be scrutinized.
Manual Test Execution Techniques
Even with strong automation, manual exploratory testing remains valuable for discovering usability glitches, accessibility problems, or edge conditions that automated scripts might miss because they follow a rigid script.
Session‑Based Test Management
- Charter – Define a short mission, e.g., “Verify that promo‑code error handling is clear for users with color‑blindness.”
- Time‑box – Allocate 45‑minute sessions; note observations in a shared sheet.
- Debrief – After each session, categorize findings (bug, suggestion, question) and decide whether to add a new automated case.
Heuristics to Apply
| Heuristic | What to Look For |
|---|---|
| Consistency | Does the promo‑code field behave the same on product page, cart page, and checkout page? |
| Error Message Clarity | Are messages specific (“Code expired”) vs generic (“Invalid code”)? |
| Accessibility | Can screen‑reader users announce the field label, input, and validation messages? Is there sufficient contrast for error text? |
| Keyboard Navigation | Can users tab to the field, apply the code with Enter, and navigate away without a mouse? |
| Performance | Does applying a code cause noticeable lag when the cart contains 500 items? |
| Localization | Do translated error messages fit within the UI bounds? Are date formats correct for the locale? |
Example Manual Test Script
- Precondition – Log in as a user with a pending cart of $75.
- Step – Navigate to the cart, click “Apply promo code”.
- Step – Paste a code with a leading space (
“SAVE10”). - Expected – System trims space, applies discount, shows success toast.
- Observation – Note whether the toast appears, whether the total updates instantly, and whether any announcement is read by VoiceOver/TalkBack.
Document the outcome, any deviation, and attach a screenshot or screen‑recording. Over several sessions, patterns emerge (e.g., the system consistently fails to trim trailing spaces on iOS Safari), prompting a targeted automated test.
Automated Test Implementation (With Code Snippets)
Automation gives you regression safety and the ability to run hundreds of promo‑code variations on each commit. Below are patterns for UI‑level, API‑level, and contract‑level tests.
UI‑Level with Playwright (Web)
const { test, expect } = require('@playwright/test');
test.describe('Promo code UI flow', () => {
test('valid percentage discount applies correctly', async ({ page }) => {
await page.goto('https://shop.example.com/cart');
await page.fill('#promo-input', 'SAVE10');
await page.click('#apply-promo');
// Wait for the total to update
const totalLocator = page.locator('.order-total');
await expect(totalLocator).toHaveText(/\\$90\\.00/);
// Verify success message
const msg = page.locator('.promo-success');
await expect(msg).toContainText('Promo code applied');
});
test('expired code shows error', async ({ page }) => {
await page.goto('https://shop.example.com/cart');
await page.fill('#promo-input', 'OLD20');
await page.click('#apply-promo');
const error = page.locator('.promo-error');
await expect(error).toHaveText(/Code has expired/);
});
});
*Why Playwright?* It handles network waiting, auto‑retries, and provides built-in locators that are resilient to minor UI changes.
API‑Level with REST Assured (Java)
If your promo‑code logic is exposed via an endpoint like POST /api/promos/apply, you can test the core calculation without a browser:
@Test
void applyValidFlatRatePromo() {
PromoRequest req = new PromoRequest()
.setCode("FLAT5")
.setCartTotalCents(2000) // $20.00
.setCurrency("USD");
Response res = given()
.contentType(ContentType.JSON)
.body(req)
.when()
.post("/api/promos/apply")
.then()
.statusCode(200)
.extract()
.response();
PromoResponse resp = res.as(PromoResponse.class);
assertEquals(1500, resp.getNewTotalCents()); // $20 - $5 = $15
assertTrue(resp.isSuccess());
assertEquals("Promo code applied", resp.getMessage());
}
*Advantages* – Fast execution (sub‑second), easy to data‑drive with CSV or JSON sources, and straightforward to integrate into CI pipelines.
Contract Testing with Pact
To guard against drift between the promo‑code service and its consumers (e.g., the frontend or a mobile app), define a contract:
const { Pact } = require('@pact-foundation/pact');
const provider = new Pact({
consumer: 'ShopFrontend',
provider: 'PromoService',
port: 1234,
log: process.cwd() + '/logs/pact.log',
});
describe('Promo Service Contract', () => {
describe('apply promo code', () => {
const expectedResponse = {
success: true,
newTotalCents: 9000,
message: 'Promo code applied',
};
before(() => provider.setup());
after(() => provider.finalize());
it('returns correct discount for valid code', () => {
return provider
.uponReceiving('a request to apply SAVE10')
.withRequest({
method: 'POST',
path: '/apply',
headers: { 'Content-Type': 'application/json' },
body: { code: 'SAVE10', cartTotalCents: 10000 },
})
.willRespondWith({
status: 200,
body: expectedResponse,
})
.then(() => {
// actual call to the service
return fetch('http://localhost:1234/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: 'SAVE10', cartTotalCents: 10000 }),
})
.then(res => res.json())
.then(body => {
expect(body).toEqual(expectedResponse);
});
});
});
});
});
Running the contract test on each build ensures that any change to the service’s API is immediately flagged by the consumer teams.
Data‑Driven Approach
Store test data in external files (CSV, JSON, or Excel) and let your test framework iterate over rows. Example with pytest and pandas:
import pandas as pd
import pytest
data = pd.read_csv('promo_cases.csv') # columns: code, total_cents, expected_cents, description
@pytest.mark.parametrize('row', data.to_dict('records'))
def test_promo_apply(row):
service = PromoService()
order = Order.create(total_cents=row['total_cents'])
result = service.apply_promo(order, row['code'])
assert result.success
assert order.total_cents == row['expected_cents']
The CSV might contain hundreds of rows covering positive, negative, edge, and boundary values, letting you expand coverage without adding test methods.
Leveraging Autonomous Exploration with SUSA
While scripted tests verify known paths, autonomous exploration can surface unexpected interactions—such as a promo‑code field that remains active after a payment timeout, or a race condition where a rapid double‑tap applies a single‑use code twice. SUSA (SUSATest) offers a no‑script mode that exercises the app using varied user personas.
How to Integrate SUSA into Your Promotion‑Code Test Strategy
- Upload the APK or provide the web URL – Point SUSA at your staging build.
- Select relevant personas – For promo codes, include:
- *Novice* – likely to mistype or paste incorrectly.
- *Impatient* – may tap Apply repeatedly.
- *Adversarial* – will attempt SQL‑injection‑style strings or extremely long inputs.
- *Accessibility* – relies on screen readers and keyboard navigation.
- Define success criteria – SUSA can be instructed to treat any HTTP 500, crash, ANR, or UI element stuck in a loading state as a failure. Additionally, you can add custom checks (via JavaScript snippets) that validate the order total after a promo attempt.
- Run a baseline session – Let SUSA explore for 10‑15 minutes. Review the generated report for:
- Crashes or ANRs triggered by promo‑code input.
- Accessibility violations (e.g., missing ARIA labels on the discount field).
- Usability friction (e.g., the Apply button remains disabled after a valid code is entered).
- Convert findings to test cases – Each distinct issue uncovered by SUSA becomes a new manual or automated test case. For example, if SUSA finds that entering a 250‑character string causes the backend to return a 500, add a boundary test for maximum input length.
- Enable cross‑session learning – On subsequent runs, SUSA avoids re‑exploring already‑verified happy paths and focuses on new edge combinations, making each execution more efficient.
CLI Example
# Install the agent
pip install susatest-agent
# Run a web test with a custom JavaScript check
susatest run \
--url https://staging.shop.example.com \
--personas novice impatient adversarial accessibility \
--js-check "return document.querySelector('.order-total').innerText.includes('$');" \
--output-dir ./susa-reports
The --js-check argument lets you assert that the order total is still a monetary value after each interaction, catching cases where the promo‑code field corrupts the DOM.
By coupling your deterministic test matrix with SUSA’s exploratory runs, you achieve both confirmation (the scripted checks that the spec is met) and discovery (the unknown‑unknowns that only appear under varied, realistic usage).
Checklist for Promo‑Code Test Cases
Use this concise list before signing off a test suite:
- [ ] All requirement IDs have at least one linked test case.
- [ ] Positive cases cover each discount type (percent, flat, free‑shipping, BOGO).
- [ ] Negative cases include malformed, expired, future, exceed‑limit, ineligible‑user, and excluded‑product scenarios.
- [ ] Edge cases test min/max length, leading/trailing spaces, case sensitivity, zero/100 % discounts, and min/max cart values.
- [ ] Boundary cases validate percentage/total limits, usage‑limit counters, date‑time inclusivity, and timezone handling.
- [ ] Data setup isolates promo‑code service, uses transactions/snapshots, and mocks external gateways.
- [ ] Prioritization (P0‑P3) reflects business impact; P0 cases are automated first.
- [ ] Traceability matrix is up‑to‑date and stored in version control.
- [ ] Automated scripts are data‑driven, independent, and run on every commit.
- [ ] Manual exploratory sessions have been conducted with at least three personas.
- [ ] SUSA (or similar autonomous tool) has been run and its findings converted to test cases.
- [ ] Test reports include pass/fail rates, performance metrics, and any accessibility violations.
Closing Takeaways
Writing test cases for promo codes is not merely about checking that a discount appears; it is about safeguarding revenue, ensuring compliance, and delivering a frictionless experience for every kind of shopper. Start by dissecting the functional specification into discrete validation, calculation, and presentation rules. Build a comprehensive matrix that spans positive, negative, edge, and boundary conditions, and tie each case back to a requirement ID for traceability. Prepare a clean, repeatable test environment with isolated data, transactional rollbacks, and mocked external services. Prioritize your effort using a risk‑based scale (P0‑P3) so that the most consequential defects are caught early and automated. Complement your scripted suite with manual exploratory sessions and autonomous exploration tools like SUSA to uncover hidden flaws that only manifest under real‑world, varied user behavior. Finally, institutionalize the practice with a living checklist, version‑controlled test artifacts, and regular review cycles so that as promo‑code rules evolve, your test suite evolves with them—keeping both the business and its users protected.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free