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.
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.
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ề.
-- 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.
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.
// 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 };
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!