How to Write Test Cases for Promo Codes (With Examples)

How to Write Test Cases for Promo Codes (With Examples)

June 27, 2026 · 14 min read · How-To Guides

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:

FieldPurposeExample Content
IDUnique identifier for traceabilityTC‑PROMO‑001
TitleShort, readable summaryVerify that a valid 10 % discount code reduces order total correctly
PreconditionsState that must exist before steps are executedUser is logged in, cart contains two items worth $100, promo‑code service is reachable
StepsOrdered actions the tester or automation performsscript performs1. Navigate to checkout page.
2. Enter promo code “SAVE10” in the discount field.
3. Click “Apply”.
4. Observe order summary.
Expected ResultObservable outcome that determines pass/failOrder total shows $90 (10 % off $100) and a success message “Promo code applied”.
Test DataSpecific values used in steps (can be referenced from a data sheet)Promo code = SAVE10, discount type = percentage, value = 10
PostconditionsState the system should be left in (optional)Promo code marked as used once for this user session.
Automation NotesHints for turning the case into a scriptUse 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.

Negative Cases

These confirm that the system rejects invalid input or ineligible scenarios.

Edge Cases

These sit at the boundaries of input domains or business rules.

Boundary Cases

These focus on numeric limits and temporal boundaries.

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:

  1. 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.
  2. Load reference data sets – Create CSV or JSON fixtures that contain:
  1. 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.
  2. 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.
  3. Parameterize environment variables – Store endpoints, API keys, and feature flags outside the test code (e.g., in a .env file) so the same test suite can run against dev, staging, and production‑like environments without modification.
  4. 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.

PriorityDefinitionTypical Promo‑Code Scenarios
P0Show‑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.
P1High 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.
P2Medium impact; edge‑case or cosmetic issues.• Leading/trailing spaces in code are not trimmed.
• Case‑sensitivity mismatch.
• Success message wording typo.
P3Low 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 IDDescriptionLinked Test‑Case IDs
REQ‑PROMO‑001Promo code must reduce order total by the specified percentage or amount.TC‑PROMO‑001, TC‑PROMO‑002, TC‑PROMO‑003
REQ‑PROMO‑002Code must be rejected if expired or not yet active.TC‑PROMO‑010, TC‑PROMO‑011
REQ‑PROMO‑003Usage limit per code must be enforced.TC‑PROMO‑015, TC‑PROMO‑016
REQ‑PROMO‑004Code eligibility based on user segment (new‑user, VIP, geo).TC‑PROMO‑020, TC‑PROMO‑021
REQ‑PROMO‑005Discount must not be applicable to excluded product categories.TC‑PROMO‑025, TC‑PROMO‑026
REQ‑PROMO‑006System 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

Heuristics to Apply

HeuristicWhat to Look For
ConsistencyDoes the promo‑code field behave the same on product page, cart page, and checkout page?
Error Message ClarityAre messages specific (“Code expired”) vs generic (“Invalid code”)?
AccessibilityCan screen‑reader users announce the field label, input, and validation messages? Is there sufficient contrast for error text?
Keyboard NavigationCan users tab to the field, apply the code with Enter, and navigate away without a mouse?
PerformanceDoes applying a code cause noticeable lag when the cart contains 500 items?
LocalizationDo translated error messages fit within the UI bounds? Are date formats correct for the locale?

Example Manual Test Script

  1. Precondition – Log in as a user with a pending cart of $75.
  2. Step – Navigate to the cart, click “Apply promo code”.
  3. Step – Paste a code with a leading space ( “SAVE10”).
  4. Expected – System trims space, applies discount, shows success toast.
  5. 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

  1. Upload the APK or provide the web URL – Point SUSA at your staging build.
  2. Select relevant personas – For promo codes, include:
  1. 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.
  2. Run a baseline session – Let SUSA explore for 10‑15 minutes. Review the generated report for:
  1. 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.
  2. 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:

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