Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

QA Automation for E-commerce: Testing Payments, Cart Flows, Inventory (2026)

April 1, 2026 EST. READ: 15 min read MIN #Quality Assurance

QA Automation for E-commerce: Testing Payments, Cart Flows, Inventory (2026)


Quick Answer: E-commerce testing adds 3 critical workflows beyond standard SaaS: (1) Payment processing (Stripe/PayPal with test cards), (2) Cart → Inventory sync (race conditions, stock levels), (3) Order fulfillment (order confirmation, email, shipping integration). Budget 30% more test cases than SaaS. Use Playwright for UI, mock payment gateways in CI, but test real payments in staging weekly.


Why Standard SaaS QA Fails for E-commerce

Standard SaaS test: Login → Feature A → Feature B → Logout ✅

E-commerce test: Login → Browse → Add to cart → Apply coupon → Shipping address → Billing address → Payment processing → Order confirmation → Email verification → Inventory updated → Shipping label generated → Customer account updated

What breaks:

  • Race conditions between cart and inventory
  • Payment gateway timeouts (Stripe, PayPal)
  • Tax calculation errors (varies by state/country)
  • Discount/coupon logic edge cases
  • Order confirmation emails never arrive
  • Inventory doesn't decrease after purchase
  • Shipping costs calculated wrong

The E-commerce Testing Pyramid

        [Payment Processing Tests]
       (Real gateway, staging only)
              (5% of tests)
            /            \
      [Inventory Tests]  [Fulfillment Tests]
     (Stock sync, race  (Order confirmation,
      conditions)        Email, Shipping)
      (15% of tests)     (10% of tests)
          \            /
       [Cart Flow Tests]
      (Add, remove, coupon)
         (40% of tests)
           /        \
     [Browse]      [Checkout]
    (Search,       (Address, Tax,
   Filter,        Shipping cost)
   Product info)   (30% of tests)

Critical E-commerce Workflows

Workflow 1: Browse → Add to Cart → Checkout

import { test, expect } from '@playwright/test';

test('customer can checkout successfully', async ({ page }) => {
  // Browse products
  await page.goto('/products');
  await page.fill('[data-testid="search"]', 'laptop');
  await page.click('button:has-text("Search")');
  
  // Verify search results
  const productCard = page.locator('[data-testid="product-card"]').first();
  await expect(productCard).toContainText('Laptop');
  
  // Click product
  await productCard.click();
  
  // Verify product details
  await expect(page.locator('h1')).toContainText('Laptop');
  const price = await page.locator('[data-testid="price"]').textContent();
  expect(parseFloat(price)).toBeGreaterThan(0);
  
  // Add to cart
  await page.fill('[data-testid="quantity"]', '2');
  await page.click('button:has-text("Add to Cart")');
  
  // Verify cart updated
  await expect(page.locator('[data-testid="cart-count"]')).toContainText('2');
  
  // Go to cart
  await page.goto('/cart');
  
  // Verify cart items
  const cartItems = page.locator('[data-testid="cart-item"]');
  await expect(cartItems).toHaveCount(1);
  
  // Checkout
  await page.click('button:has-text("Proceed to Checkout")');
  await expect(page).toHaveURL(/.*checkout/);
});

Workflow 2: Payment Processing (Stripe)

test('customer can pay with credit card', async ({ page }) => {
  // Setup: User in checkout with total $99.99
  await page.goto('/checkout');
  
  // Fill shipping address
  await page.fill('[data-testid="email"]', 'customer@example.com');
  await page.fill('[data-testid="full-name"]', 'John Doe');
  await page.fill('[data-testid="address"]', '123 Main St');
  await page.fill('[data-testid="city"]', 'New York');
  await page.selectOption('[data-testid="state"]', 'NY');
  await page.fill('[data-testid="zip"]', '10001');
  
  // Select shipping method
  await page.click('input[value="standard"]');  // Standard shipping
  
  // Verify shipping cost added
  const shippingCost = await page.locator('[data-testid="shipping-cost"]').textContent();
  expect(parseFloat(shippingCost)).toBe(9.99);
  
  // Verify total updated
  const total = await page.locator('[data-testid="total"]').textContent();
  expect(parseFloat(total)).toBe(109.98);  // $99.99 + $9.99 shipping
  
  // Stripe payment form (iframe)
  const frameHandle = await page.$('iframe[name="__privateStripeFrame"]');
  const frame = await frameHandle.contentFrame();
  
  // Use Stripe test card (always succeeds)
  await frame.fill('[placeholder="Card number"]', '4242 4242 4242 4242');
  await frame.fill('[placeholder="MM / YY"]', '12/25');
  await frame.fill('[placeholder="CVC"]', '123');
  
  // Submit payment
  await page.click('button:has-text("Pay Now")');
  
  // Verify success
  await page.waitForURL(/.*order-confirmation/);
  const orderNum = await page.locator('[data-testid="order-number"]').textContent();
  expect(orderNum).toMatch(/^ORD-\d+/);
});

test('payment fails with invalid card', async ({ page }) => {
  // Setup: User in checkout
  await page.goto('/checkout');
  
  // Fill address...
  // (same as above)
  
  // Stripe payment with declined card
  const frameHandle = await page.$('iframe[name="__privateStripeFrame"]');
  const frame = await frameHandle.contentFrame();
  
  // Test card that always declines
  await frame.fill('[placeholder="Card number"]', '4000 0000 0000 0002');
  await frame.fill('[placeholder="MM / YY"]', '12/25');
  await frame.fill('[placeholder="CVC"]', '123');
  
  // Submit payment
  await page.click('button:has-text("Pay Now")');
  
  // Verify error
  await expect(page.locator('[role="alert"]')).toContainText('Card declined');
  await expect(page).toHaveURL(/.*checkout/);  // Still on checkout
});

Workflow 3: Coupon/Discount Application

test('customer can apply coupon code', async ({ page }) => {
  // Setup: In cart with $100 subtotal
  await page.goto('/cart');
  
  // Verify initial subtotal
  let subtotal = await page.locator('[data-testid="subtotal"]').textContent();
  expect(parseFloat(subtotal)).toBe(100.00);
  
  // Apply coupon
  await page.fill('[data-testid="coupon-code"]', 'SAVE20');
  await page.click('button:has-text("Apply Coupon")');
  
  // Verify discount applied
  await expect(page.locator('[data-testid="discount-amount"]')).toContainText('-$20.00');
  
  // Verify total updated
  const total = await page.locator('[data-testid="total"]').textContent();
  expect(parseFloat(total)).toBe(80.00);  // $100 - $20 discount
});

test('coupon code has usage limits', async ({ page }) => {
  // Setup: Coupon with max 5 uses, already used 5 times
  await page.goto('/cart');
  
  await page.fill('[data-testid="coupon-code"]', 'LIMIT5');
  await page.click('button:has-text("Apply Coupon")');
  
  // Verify error
  await expect(page.locator('[role="alert"]')).toContainText('Coupon usage limit reached');
});

Workflow 4: Inventory Sync (Race Condition Testing)

test('inventory decreases after purchase', async ({ page, context }) => {
  // Setup: Get initial stock count
  const productId = 'laptop-123';
  let initialStock = await getInventoryCount(productId);
  expect(initialStock).toBe(10);
  
  // Customer 1: Add to cart and checkout
  const page1 = page;
  await page1.goto(`/products/${productId}`);
  await page1.fill('[data-testid="quantity"]', '2');
  await page1.click('button:has-text("Add to Cart")');
  await completeCheckout(page1, 'customer1@example.com');
  
  // Wait for inventory to sync
  await page1.waitForTimeout(2000);
  
  // Verify inventory decreased by 2
  let currentStock = await getInventoryCount(productId);
  expect(currentStock).toBe(8);  // 10 - 2
});

test('prevent overselling with race condition', async ({ page, context }) => {
  // Setup: Only 1 item in stock
  const productId = 'rare-item';
  await setInventory(productId, 1);
  
  // Customer 1: Starts checkout
  const page1 = page;
  await page1.goto(`/products/${productId}`);
  await page1.fill('[data-testid="quantity"]', '1');
  await page1.click('button:has-text("Add to Cart")');
  
  // Customer 2: Simultaneously starts checkout (in parallel browser)
  const page2 = await context.newPage();
  await page2.goto(`/products/${productId}`);
  await page2.fill('[data-testid="quantity"]', '1');
  await page2.click('button:has-text("Add to Cart")');
  
  // Customer 1 completes payment
  await completeCheckout(page1, 'customer1@example.com');
  
  // Customer 2 tries to complete payment
  await completeCheckout(page2, 'customer2@example.com');
  
  // One should fail (out of stock)
  const error = await page2.locator('[role="alert"]').textContent();
  expect(error).toContain('out of stock');
  
  // Verify only 1 sold
  const finalStock = await getInventoryCount(productId);
  expect(finalStock).toBe(0);
});

async function getInventoryCount(productId) {
  const response = await fetch(`http://localhost:3000/api/products/${productId}`);
  const data = await response.json();
  return data.stock;
}

Workflow 5: Order Confirmation Email

test('customer receives order confirmation email', async ({ page }) => {
  const customerEmail = `qa-test-${Date.now()}@mailosaur.net`;
  
  // Complete checkout
  await page.goto('/checkout');
  await page.fill('[data-testid="email"]', customerEmail);
  // Fill address and payment...
  await page.click('button:has-text("Pay Now")');
  
  // Wait for order confirmation
  await page.waitForURL(/.*order-confirmation/);
  
  // Check email via Mailosaur API
  const client = new MailosaurClient(process.env.MAILOSAUR_API_KEY);
  
  // Wait for email (with timeout)
  const messages = await client.messages.query(
    process.env.MAILOSAUR_SERVER_ID,
    { sentTo: customerEmail },
    { timeout: 10000 }
  );
  
  expect(messages.items.length).toBeGreaterThan(0);
  
  const confirmationEmail = messages.items[0];
  expect(confirmationEmail.subject).toContain('Order Confirmation');
  expect(confirmationEmail.html.body).toContain('Thank you for your purchase');
  
  // Extract order number from email
  const orderNum = confirmationEmail.html.body.match(/Order #(\d+)/)[1];
  expect(orderNum).toBeTruthy();
});

E-commerce Specific Test Cases

Tax Calculation Testing

test('tax calculated correctly by state', async ({ page }) => {
  // California (8.625% tax)
  await page.goto('/checkout');
  await page.selectOption('[data-testid="state"]', 'CA');
  await page.fill('[data-testid="zip"]', '94102');
  
  let subtotal = 100.00;
  let tax = await page.locator('[data-testid="tax"]').textContent();
  expect(parseFloat(tax)).toBeCloseTo(8.625, 2);
  
  // Change to Oregon (0% sales tax)
  await page.selectOption('[data-testid="state"]', 'OR');
  tax = await page.locator('[data-testid="tax"]').textContent();
  expect(parseFloat(tax)).toBe(0.00);
});

Multi-Currency Testing

test('prices display in correct currency', async ({ page, context }) => {
  // USD
  await page.goto('/products?currency=USD');
  let price = await page.locator('[data-testid="price"]').first().textContent();
  expect(price).toMatch(/\$\d+\.\d{2}/);
  
  // EUR
  await page.goto('/products?currency=EUR');
  price = await page.locator('[data-testid="price"]').first().textContent();
  expect(price).toMatch(/€\d+,\d{2}/);
  
  // GBP
  await page.goto('/products?currency=GBP');
  price = await page.locator('[data-testid="price"]').first().textContent();
  expect(price).toMatch(/£\d+\.\d{2}/);
});

Shipping Cost Calculation

test('shipping cost varies by method and weight', async ({ page }) => {
  // Standard: $9.99 for any order
  await selectShippingMethod(page, 'standard');
  let cost = await page.locator('[data-testid="shipping-cost"]').textContent();
  expect(parseFloat(cost)).toBe(9.99);
  
  // Express: $19.99 for any order
  await selectShippingMethod(page, 'express');
  cost = await page.locator('[data-testid="shipping-cost"]').textContent();
  expect(parseFloat(cost)).toBe(19.99);
  
  // Overnight: Varies by weight
  await selectShippingMethod(page, 'overnight');
  // $35 base + $5 per lb
  // Assuming 5 lb item total: $60
  cost = await page.locator('[data-testid="shipping-cost"]').textContent();
  expect(parseFloat(cost)).toBe(60.00);
});

CI/CD Pipeline for E-commerce

# .github/workflows/ecommerce-tests.yml
name: E-commerce QA Tests

on: [push, pull_request]

jobs:
  unit-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      
      - name: Install dependencies
        run: npm ci
      
      - name: Run unit tests
        run: npm run test:unit
      
      - name: Run integration tests (mock payment)
        run: npm run test:integration
        env:
          STRIPE_TEST_KEY: sk_test_...
  
  checkout-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      
      - name: Start server
        run: npm run dev &
      
      - name: Run Playwright checkout tests
        run: npx playwright test tests/e2e/checkout.spec.js
        env:
          BASE_URL: http://localhost:3000
  
  payment-staging:
    runs-on: ubuntu-latest
    # Only on merge to main (not on every PR)
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      
      - name: Run payment tests in staging
        run: npx playwright test tests/e2e/payments.spec.js
        env:
          ENVIRONMENT: staging
          STRIPE_ACCOUNT: acct_test_...
          MAILOSAUR_KEY: ${{ secrets.MAILOSAUR_API_KEY }}
      
      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Payment tests failed in staging'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Red Flags in E-commerce Testing

🚩 Red Flag 1: No inventory lock during checkout

Problem: Customer A adds last item to cart, but Customer B completes checkout first. Customer A oversells.

Solution: Implement pessimistic locking when adding to cart, or optimistic locking at payment time.

🚩 Red Flag 2: Email confirmation never arrives

Problem: Order completes, but customer never gets confirmation email. Causes support tickets.

Solution: Test email delivery on EVERY order. Use Mailosaur/Mailtrap to verify in CI.

🚩 Red Flag 3: No test data cleanup

Problem: Tests create fake orders in production database. Affects inventory counts, revenue reports.

Solution: Use test mode in payment gateway, mark test orders with flag, clean up nightly.

🚩 Red Flag 4: Tax calculation edge cases untested

Problem: 5% of orders have wrong tax. Customers complain, support scrambles.

Solution: Test every state, every country. Tax rules change—add tests when you expand to new regions.

🚩 Red Flag 5: No load testing during checkout

Problem: Black Friday traffic → checkout crashes → 50% order completion rate.

Solution: Load test checkout with 1000+ concurrent users weekly. Simulate payment gateway latency.


FAQ: E-commerce Testing

Q: Should I test in production or staging?

A: Staging for everything EXCEPT real payment processing. Test payments in staging weekly (use real Stripe test account), but never in production unless it's a real customer order.

Q: How do I mock payment gateways in CI?

A: Use Stripe/PayPal test mode. They provide test cards that simulate success/failure. Use them in CI.

Q: How do I prevent race conditions in inventory?

A: Database-level locking (FOR UPDATE), or optimistic locking with versioning. Test both approaches under load.

Q: How often should I test real payments?

A: Weekly in staging. Daily in production (one real test order per day). Use isolated test accounts.

Q: What about testing third-party integrations (shipping, analytics)?

A: Use APIs' sandbox/test environments. Mock in unit tests, use real sandbox in integration tests.


Bottom Line

E-commerce testing is 30-40% more complex than standard SaaS because:

  1. Payment processing (real money, regulatory compliance)
  2. Inventory sync (race conditions, overselling)
  3. Tax/shipping calculations (geographic complexity)
  4. Order fulfillment (email, shipping labels, tracking)
  5. Financial auditing (every dollar must be accounted for)

Focus on these 5 workflows first:

  1. Browse → Cart → Checkout
  2. Payment processing (success + failure cases)
  3. Coupon/discount application
  4. Inventory sync
  5. Order confirmation email

Do this and you'll catch 80% of e-commerce bugs before they hit production.

Tayyab Akmal
// author

Tayyab Akmal

AI & QA Automation Engineer

6 years of catching critical bugs in fintech, e-commerce, and SaaS — then building the Playwright and Selenium automation that prevents them from shipping again.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire