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
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Parallel checkout | qty=1, two workers | Both succeed | Seed qty=1 per run; probe one order |
| Reservation hold | 15-min TTL | Checkout fails at pay | Seed short TTL; clock advance |
| Release on abandon | Cart deleted | Hold never freed | Probe available qty after timeout |
| Backorder | negative allowed? | Wrong messaging | Probe stock_state + policy flag |
| Multi-warehouse | Ship from B only | Wrong allocation | Seed warehouse qty; probe pick list |
| Bundle SKU | Component stock | Bundle sells when component 0 | Probe component decrement |
| Webhook lag | Payment then allocate | Double allocate | Idempotent allocation handler |
| Cancel order | Restock | Qty wrong | Probe restock event |
| Pre-order | Future ship date | Decrement timing | Probe decrement at ship not order |
| Flash sale | Rate limit | 503 untested | Load test separate; E2E one race |
| Guest cart merge | Two holds same SKU | Over-reserve | Probe 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_state | Assert |
|---|---|
in_stock | Add to cart succeeds; probe reserved |
out_of_stock | Probe blocks checkout; UI optional |
backorder | Order 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-pattern | Why it fails | Better approach |
|---|---|---|
| Shared staging SKU qty=999 | Hides races | Per-run seed qty=1 |
| UI "Out of stock" only | API still sells | Probe checkout API 409 |
| Fixed sleep after add-to-cart | TTL flake | Poll reserved count |
| No restock test | Drifting qty | Probe 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.
- Arrange: Seed SKU qty=1 scoped to runId.
- Act: Parallel checkout attempts.
- 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).
Related scenarios
- Marketplace flows — vendor stock split
- Stripe payments — pay then allocate
- Shipping fulfillment — pick/pack after allocate
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.