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:
- Payment processing (real money, regulatory compliance)
- Inventory sync (race conditions, overselling)
- Tax/shipping calculations (geographic complexity)
- Order fulfillment (email, shipping labels, tracking)
- Financial auditing (every dollar must be accounted for)
Focus on these 5 workflows first:
- Browse → Cart → Checkout
- Payment processing (success + failure cases)
- Coupon/discount application
- Inventory sync
- Order confirmation email
Do this and you'll catch 80% of e-commerce bugs before they hit production.
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.