Skip to main content

How to Test Inventory Updates and Stock Reservations

Short answer

Inventory bugs oversell when parallel CI workers share one staging SKU, or when tests assert "Sold out" UI while the database still decrements on race. Seed per-run SKU quantity via API Arrange, use probe Assert for authoritative stock and order counts, and test reservation TTL with clock fixtures—not waitForTimeout after add-to-cart.

Part of Testing Guides by business flow.

Who this is for

Ecommerce, marketplace, and ops teams with real-time stock, cart reservations, or multi-warehouse fulfillment who run Playwright against checkout—not spreadsheets updated manually in staging.

Why testing inventory matters

  • Revenue loss — overselling forces cancellations and chargebacks; promotions spike concurrent checkouts.
  • Trust — customers see "In stock" then get post-order email delays.
  • Ops cost — ghost reservations block inventory until TTL expires untested.

Shared staging catalog with qty=100 hides race bugs that prod hits on limited drops.

Complexity map

ScenarioEdge caseWhy tests breakApproach
Parallel checkoutqty=1, two workersBoth succeedSeed qty=1 per run; probe one order
Reservation hold15-min TTLCheckout fails at paySeed short TTL; clock advance
Release on abandonCart deletedHold never freedProbe available qty after timeout
Backordernegative allowed?Wrong messagingProbe stock_state + policy flag
Multi-warehouseShip from B onlyWrong allocationSeed warehouse qty; probe pick list
Bundle SKUComponent stockBundle sells when component 0Probe component decrement
Webhook lagPayment then allocateDouble allocateIdempotent allocation handler
Cancel orderRestockQty wrongProbe restock event
Pre-orderFuture ship dateDecrement timingProbe decrement at ship not order
Flash saleRate limit503 untestedLoad test separate; E2E one race
Guest cart mergeTwo holds same SKUOver-reserveProbe single hold per user+sku

Arrange: seed stock per run

// POST /api/test/seed-inventory
// { runId, sku: `E2E-${runId}`, quantity: 1, warehouse: 'WH-A', reservationTtlSeconds: 900 }

await request.post('/api/test/seed-inventory', {
data: { runId, quantity: 1, reservationTtlSeconds: 60 },
});

Never mutate shared catalog SKUs used by manual QA.

Oversell race (parallel workers)

test.describe.configure({ mode: 'parallel' });

test('only one checkout succeeds for qty=1', async ({ request }) => {
const sku = `RACE-${runId}`;
await request.post('/api/test/seed-inventory', { data: { runId, sku, quantity: 1 } });

const attempt = () => request.post('/api/test/checkout-fast', { data: { runId, sku } });

const [a, b] = await Promise.all([attempt(), attempt()]);
const statuses = [await a.json(), await b.json()];
const paid = statuses.filter(s => s.status === 'paid');
expect(paid).toHaveLength(1);

const stock = await request.get(`/api/test/probes/inventory/${sku}`).then(r => r.json());
expect(stock.available).toBe(0);
});

UI path optional—API race proves allocator; add one Playwright spec for full cart UX.

Reservation timeout

await request.post('/api/test/seed-inventory', {
data: { runId, sku, quantity: 5, reservationTtlSeconds: 5 },
});
await page.goto(`/product/${sku}`);
await page.getByRole('button', { name: 'Add to cart' }).click();

await expect.poll(async () => {
const res = await request.get(`/api/test/probes/inventory/${sku}`);
return (await res.json()).reserved;
}).toBe(1);

// Advance clock or wait TTL via test hook
await request.post('/api/test/advance-time', { data: { seconds: 6 } });

await expect.poll(async () => {
const res = await request.get(`/api/test/probes/inventory/${sku}`);
return (await res.json()).reserved;
}).toBe(0);

Backorder and out-of-stock

stock_stateAssert
in_stockAdd to cart succeeds; probe reserved
out_of_stockProbe blocks checkout; UI optional
backorderOrder allowed; probe ship_date set

Integration with payments

After Stripe payment, probe final inventory decrement once webhook confirms paid—not on cart add alone.

Anti-patterns

Anti-patternWhy it failsBetter approach
Shared staging SKU qty=999Hides racesPer-run seed qty=1
UI "Out of stock" onlyAPI still sellsProbe checkout API 409
Fixed sleep after add-to-cartTTL flakePoll reserved count
No restock testDrifting qtyProbe after cancel

Example scenario

Situation: Two shoppers checkout the last unit simultaneously during a drop.

Expected outcome: Exactly one paid order; second receives clear failure; stock zero.

Why UI-only automation breaks: Second user sees generic error but order row created unpaid.

  1. Arrange: Seed SKU qty=1 scoped to runId.
  2. Act: Parallel checkout attempts.
  3. Assert: Probe one paid order; probe available=0; second attempt failed at payment or allocation.

TestChimp workflow: Track sku × warehouse × stock_state in TrueCoverage when multi-warehouse prod share grows.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I test overselling with parallel Playwright workers?

Seed SKU with quantity=1 scoped to run, launch two checkout tests against same SKU, probe Assert exactly one order row created. Never rely on shared catalog qty in staging.

Should inventory tests use UI or API Arrange?

API/seed Arrange for stock levels; short UI Act for checkout; probe Assert for final quantity and order lines.

How do I test reservation expiry without waiting 15 minutes?

Seed short reservationTtlSeconds and advance time via test clock endpoint or Playwright clock fixture, then poll probe until reserved returns to zero.

When should inventory decrement—at cart, pay, or ship?

Match prod business rules in Assert. Most systems reserve at cart, decrement at paid webhook, or decrement at ship—probe the authoritative transition your ops team expects.

How do multi-warehouse inventory tests differ?

Seed per-warehouse quantities; checkout with shipping zone that pulls from WH-B only; probe allocation row references correct warehouse_id.

Should I load-test flash sales in Playwright?

Use dedicated load tools for hundreds of concurrent users; Playwright proves correctness with two parallel workers on qty=1—not throughput.

How do we know all warehouses are covered?

Compare sku × warehouse × stock_state in TrueCoverage. When new fulfillment nodes ship, run /testchimp evolve to add warehouse-specific scenarios with // @Scenario: links.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo