CYBERSOFT
Đăng nhập

Thực chiến TMĐT: kiểm thử checkout trong flash sale (tồn kho & đồng thời cao)

Thực chiến doanh nghiệpTMĐTPlaywrightAPIThực tếPhỏng vấn
🗓 1 tháng trước22 phút đọc·👁 328 lượt xem👤 305 người đọc

Oracle-first cho checkout flash sale: tồn kho không âm, tổng tiền & coupon đúng, máy trạng thái đơn, thanh toán idempotent. Đồng thời cao và race condition qua Promise.all, seed bằng test-only API, mock cổng thanh toán, timeout/webhook/reconciliation, refund, CI phân tầng và ranh giới AI-agent. Kèm góc phỏng vấn.

1. Bối cảnh nghiệp vụ: flash sale là bài toán đồng thời khắc nghiệt

Flash sale là kịch bản tàn nhẫn nhất mà một hệ thống thương mại điện tử phải chịu. Trong một sàn có quy mô mười triệu người dùng, một chương trình giảm giá lúc mười hai giờ trưa có thể kéo hai trăm nghìn người bấm mua cùng một sản phẩm chỉ còn năm nghìn suất trong vòng ba giây. Đây không phải bài toán tải thuần tuý mà là bài toán tính đúng đắn dưới đồng thời cao: hệ thống phải bán đúng năm nghìn suất, không một suất nào hơn, không âm tồn kho, và mỗi khách trả tiền đúng số tiền của giỏ hàng mình. Một lỗi nhỏ ở đây không chỉ gây khó chịu mà tạo thiệt hại tài chính thật và khủng hoảng niềm tin.

Bài viết này tiếp cận checkout flash sale như một bài toán kiểm thử oracle-first ở quy mô doanh nghiệp, không phải một luồng click đơn giản. Chúng ta sẽ xác định các bất biến nghiệp vụ làm oracle, dựng kiến trúc và mô hình dữ liệu, viết test plan và ma trận ca, rồi đi từ happy path đến các ca thất bại sâu: oversell do đua tranh, tính sai tổng tiền, coupon bị lạm dụng, thanh toán trùng lặp, timeout và refund. Cuối cùng là CI, ranh giới cho AI-agent và góc phỏng vấn. Mọi ví dụ dùng Playwright lái giao diện kết hợp request context để bơm seed và kiểm bất biến ở backend.

Quy mô giả định xuyên suốt: 10 triệu tài khoản, đỉnh 200.000 request/giây khi mở bán, SLA thanh toán p99 < 800ms, tuân thủ PCI-DSS cho dữ liệu thẻ.
KIẾN TRÚC CHECKOUT · FLASH SALE Client / Appgiỏ hàng, checkout Order APIreserve → confirm Inventorystock có khoá Payment GWidempotent + hook Coupon svcgiới hạn lượt dùng Oracle: tồn kho không âm · tổng tiền đúng · order state hợp lệ · payment idempotent Playwright lái UI + request context bơm seed và kiểm bất biến ở backend.
Kiến trúc checkout flash sale và bốn bất biến làm oracle.

2. Bất biến làm oracle: bốn điều không bao giờ được sai

Oracle là câu trả lời cho câu hỏi 'kết quả đúng là gì?'. Với checkout flash sale, đừng bao giờ khẳng định 'màn hình hiện thành công' vì màn hình có thể xanh trong khi backend đã bán quá số lượng. Thay vào đó, ta khẳng định bốn bất biến nghiệp vụ. Thứ nhất, tồn kho không bao giờ âm và tổng số suất bán ra không vượt tồn ban đầu — đây là chống oversell. Thứ hai, tổng tiền giỏ hàng và coupon phải đúng đến từng đồng theo bảng giá. Thứ ba, đơn hàng chỉ đi theo các chuyển trạng thái hợp lệ. Thứ tư, thanh toán phải idempotent: retry cùng một yêu cầu chỉ tạo đúng một khoản thu.

Bốn bất biến viết thành mệnh đề kiểm được

  • Không oversell: SUM(units_sold) ≤ initial_stock và stock_remaining ≥ 0 tại mọi thời điểm.
  • Tổng tiền đúng: order.total = SUM(line.qty × line.unit_price) − discount(coupon), không sai số làm tròn.
  • State machine: mọi order.status_transition nằm trong tập chuyển hợp lệ; không nhảy cóc.
  • Idempotent payment: với cùng idempotency_key, số bản ghi charge = 1 dù retry N lần.
💡 Viết oracle dưới dạng truy vấn SQL hoặc endpoint kiểm tra chuyên dụng, rồi để Playwright gọi qua request context. Assert trên dữ liệu thật mạnh hơn nhiều so với đọc chữ trên màn hình.

3. Kiến trúc và mô hình dữ liệu: nơi bất biến sống

Để kiểm được các bất biến, tester phải hiểu chúng sống ở đâu trong dữ liệu. Kiến trúc điển hình gồm Order API điều phối, Inventory service giữ tồn kho có khoá, Payment gateway xử lý thu tiền idempotent, và Coupon service kiểm soát lượt dùng. Mô hình dữ liệu cốt lõi gồm bảng product với stock, bảng reservation giữ chỗ tạm khi khách vào checkout, bảng order và order_line, bảng payment với idempotency_key, và bảng coupon_redemption ghi lần dùng. Reservation là mấu chốt chống oversell: nó tạm giữ tồn trong một cửa sổ thời gian, hết hạn thì tự nhả về.

sql
-- Mô hình dữ liệu rút gọn — nơi các bất biến được thực thi
CREATE TABLE product (
  id          BIGINT PRIMARY KEY,
  sku         TEXT UNIQUE NOT NULL,
  unit_price  BIGINT NOT NULL,             -- lưu bằng đơn vị nhỏ nhất (đồng), tránh float
  stock       INT NOT NULL CHECK (stock >= 0)  -- BẤT BIẾN: tồn kho không âm
);

CREATE TABLE reservation (
  id          UUID PRIMARY KEY,
  product_id  BIGINT REFERENCES product(id),
  qty         INT NOT NULL CHECK (qty > 0),
  expires_at  TIMESTAMPTZ NOT NULL,        -- hết hạn → nhả tồn về
  status      TEXT NOT NULL                -- HELD | CONSUMED | RELEASED
);

CREATE TABLE payment (
  id              UUID PRIMARY KEY,
  order_id        UUID REFERENCES orders(id),
  idempotency_key TEXT UNIQUE NOT NULL,    -- BẤT BIẾN: một key → tối đa một charge
  amount          BIGINT NOT NULL,
  status          TEXT NOT NULL            -- PENDING | CAPTURED | FAILED | REFUNDED
);

Điểm cần nhấn cho tester: đơn giá lưu bằng số nguyên theo đơn vị nhỏ nhất (đồng), không bao giờ dùng số thực dấu phẩy động cho tiền, vì float gây sai số làm tròn tích luỹ. Ràng buộc CHECK (stock >= 0) ở tầng cơ sở dữ liệu là hàng phòng thủ cuối cùng chống oversell: dù logic ứng dụng có lỗi, database vẫn từ chối ghi âm. Cột idempotency_key có ràng buộc UNIQUE biến idempotency thành bất biến cấp lưu trữ chứ không chỉ là quy ước. Test của ta sẽ tấn công đúng những điểm này.

Tiền luôn là số nguyên đơn vị nhỏ nhất; so sánh bằng số nguyên. Bug tiền tệ do float là lỗi kinh điển mà interviewer thích hỏi.

4. Test plan: phạm vi, tầng test và chiến lược seed

Một test plan tốt phân tầng rõ ràng để mỗi loại lỗi được bắt ở tầng rẻ nhất. Tầng đơn vị kiểm logic tính tổng và áp coupon với dữ liệu giả. Tầng tích hợp API kiểm reservation, xác nhận đơn và idempotency trực tiếp qua request context, nhanh và tất định. Tầng end-to-end qua giao diện chỉ giữ một lớp mỏng cho các luồng người dùng quan trọng nhất. Tầng đồng thời là đặc thù của flash sale: bắn nhiều yêu cầu song song để phơi bày race condition mà test tuần tự không bao giờ thấy. Chiến lược seed dùng test-only API để tạo sản phẩm, đặt tồn kho chính xác và phát coupon, đảm bảo mỗi test tự cô lập.

ts
// fixtures.ts — seed qua test-only API, mỗi test tự tạo & tự dọn (idempotent)
import { test as base, expect } from '@playwright/test';

type Seed = { product: { id: string; sku: string }; coupon: { code: string } };

export const test = base.extend<{ seed: Seed }>({
  seed: async ({ request }, use, info) => {
    // ĐẶT tồn kho CHÍNH XÁC để oracle oversell kiểm được
    const p = await (await request.post('/api/test/products', {
      data: { sku: `FLASH-${info.testId}`, unitPrice: 199000, stock: 5 },
    })).json();
    const c = await (await request.post('/api/test/coupons', {
      data: { code: `SAVE-${info.testId}`, percent: 10, maxRedemptions: 3 },
    })).json();

    await use({ product: p, coupon: c });

    // teardown: dọn sạch để chạy lại vẫn tất định
    await request.delete(`/api/test/products/${p.id}`);
    await request.delete(`/api/test/coupons/${c.code}`);
  },
});
export { expect };
🔒

Nội dung bị khoá

Bạn đang xem bản xem trước. Đăng nhập để mở khoá toàn bộ bài viết và tất cả tài liệu Tester/QA.

Đã xem 25% nội dung
  • Đọc trọn vẹn mọi bài viết
  • Lưu bài & ghi chú cá nhân
  • Theo dõi tiến độ đã đọc
  • Luyện phỏng vấn, ISTQB, Mock
Đăng nhập để đọc tiếp

Chưa có mã? Lấy mã qua Fanpage / Zalo CyberSoft.

0

💬 Bình luận (0)

Bạn có thể đọc mọi bình luận. Đăng nhập để bình luận

Chưa có bình luận nào. Hãy là người đầu tiên!