1. Bối cảnh: vì sao fintech cần kiểm thử tích hợp nhiều tầng
Một nền tảng fintech chuyển tiền không giống một trang blog: mỗi request có thể di chuyển tiền thật, và một lỗi nhỏ ở tầng nào cũng có thể biến thành thiệt hại tài chính hoặc vi phạm tuân thủ. Chính vì thế, kiểm thử một tầng đơn lẻ là không đủ. Bạn có thể có một giao diện đẹp mà API trả sai số dư; bạn có thể có API đúng hợp đồng nhưng sập dưới tải giờ cao điểm; bạn có thể có hệ thống nhanh mà không ai biết vì sao một giao dịch treo. Bài viết này trình bày một chiến lược tích hợp bốn tầng — Playwright cho E2E, API tests cho hợp đồng và trạng thái, k6 cho hiệu năng, và observability làm nền chung — với AI đóng vai trò phân loại lỗi xuyên tầng thay vì thay thế người kiểm thử.
Kịch bản xuyên suốt bài là một luồng nạp tiền và chuyển khoản: người dùng đăng nhập, nạp tiền vào ví, chuyển cho người khác, và hệ thống phải giữ những bất biến tuyệt đối như tổng tiền được bảo toàn theo bút toán kép, không bao giờ tạo hai giao dịch từ một lần bấm, và số dư không bao giờ âm. Bốn tầng kiểm thử cùng bảo vệ những bất biến này ở các góc nhìn khác nhau. E2E chứng minh luồng người dùng thật hoạt động; API tests chứng minh trạng thái và hợp đồng đúng ở tốc độ cao; k6 chứng minh hệ thống giữ đúng các bất biến đó cả khi chịu tải; observability cho phép truy vết chính xác một giao dịch cụ thể khi có sự cố.
2. Chiến lược phân tầng: mỗi tầng có oracle riêng
Sức mạnh của chiến lược tích hợp nằm ở chỗ mỗi tầng có một oracle rõ ràng và khác nhau, nên chúng không trùng lặp mà bổ sung cho nhau. Oracle của tầng E2E là bất biến nghiệp vụ nhìn từ mắt người dùng: sau khi chuyển tiền, số dư hiển thị đúng và lịch sử giao dịch phản ánh đúng. Oracle của tầng API là hợp đồng và trạng thái: đúng schema, đúng mã trạng thái, và quan trọng nhất là tính idempotency — gửi lại cùng một request không tạo giao dịch thứ hai. Oracle của tầng perf là SLO: p95 độ trễ dưới ngưỡng, tỉ lệ lỗi dưới ngưỡng, throughput đạt mục tiêu. Oracle của tầng observability không phải một khẳng định pass/fail mà là khả năng truy vết: mọi giao dịch phải có trace-id đi xuyên các dịch vụ.
- E2E (Playwright): oracle = bất biến nghiệp vụ qua UI — số dư, lịch sử, thông báo đúng.
- API: oracle = hợp đồng + trạng thái + idempotency (冪等性) — schema, status, không tạo giao dịch trùng.
- Perf (k6): oracle = SLO — p95, error rate, throughput so với ngân sách đã thoả thuận.
- Observability: oracle = truy vết — mọi giao dịch có trace-id xuyên dịch vụ, log có cấu trúc.
Điểm mấu chốt là các oracle này phải nhất quán với nhau. Nếu tầng API khẳng định idempotency nhưng tầng perf lại tạo tải bằng cách gửi cùng một idempotency-key hàng nghìn lần và mong đợi hàng nghìn giao dịch, thì hai tầng đang mâu thuẫn và một trong hai có oracle sai. Khi thiết kế bộ kiểm thử tích hợp, bạn phải ngồi lại và viết ra danh sách bất biến nghiệp vụ chung, rồi ánh xạ mỗi bất biến vào tầng phù hợp nhất để kiểm nó. Đây là công việc phán đoán của con người mà AI không tự làm thay được: AI có thể sinh test cho từng tầng, nhưng chính bạn phải quyết định bất biến nào thuộc về tầng nào.
3. Chia sẻ fixture và dữ liệu giữa các tầng
Một sai lầm phổ biến là mỗi tầng tự tạo dữ liệu riêng: E2E tạo một người dùng, API tests tạo người dùng khác, k6 lại tạo một tập thứ ba. Kết quả là bạn không thể tương quan kết quả giữa các tầng, và mỗi tầng mang một sự mong manh khác nhau. Cách làm đúng là tập trung việc tạo dữ liệu vào một lớp fixture dùng chung: một hàm tạo người dùng test, nạp số dư ban đầu, và trả về thông tin đăng nhập cùng các định danh mà mọi tầng đều dùng. Playwright gọi nó qua request context, k6 gọi nó qua một bước setup, và cả hai cùng thao tác trên cùng một loại tài khoản đã biết trước bất biến.
// fixtures/wallet.ts — lớp tạo dữ liệu DÙNG CHUNG cho mọi tầng
import { request as pwRequest } from '@playwright/test';
export interface SeededUser {
userId: string; email: string; pass: string;
walletId: string; initialBalance: number;
}
// Gọi từ Playwright fixture, từ setup của k6 (qua HTTP), từ CI seed.
export async function seedWallet(baseURL: string, initialBalance = 100_000): Promise<SeededUser> {
const ctx = await pwRequest.newContext({ baseURL });
const res = await ctx.post('/test-support/seed-wallet', {
data: { initialBalance },
headers: { 'x-test-support': process.env.TEST_SUPPORT_TOKEN! },
});
if (!res.ok()) throw new Error('seedWallet failed: ' + res.status());
const user = await res.json();
await ctx.dispose();
return user; // { userId, email, pass, walletId, initialBalance }
}Endpoint test-support ở trên chỉ tồn tại trong môi trường test và được bảo vệ bằng một token riêng, không bao giờ bật ở production. Nó cho phép mọi tầng tạo dữ liệu sạch một cách nhanh và tất định thay vì phải bấm qua UI để đăng ký. Nhờ tập trung như vậy, khi bạn đổi mô hình dữ liệu ví, bạn chỉ sửa một chỗ và cả bốn tầng tự động dùng dữ liệu mới. Đây cũng là nơi bạn cài đặt các bất biến ban đầu: số dư khởi tạo, trạng thái tài khoản, hạn mức — để mọi tầng bắt đầu từ cùng một điểm xuất phát đã biết.
4. Tầng E2E: Playwright kiểm luồng nạp tiền và chuyển khoản
Tầng E2E chứng minh rằng một người dùng thật có thể hoàn thành luồng nghiệp vụ và các bất biến hiển thị đúng. Điều cần tránh là viết test E2E chỉ kiểm 'có toast thành công' — đó là oracle hời hợt. Test E2E tốt cho fintech phải đọc số dư trước, thực hiện chuyển khoản qua giao diện, rồi khẳng định số dư sau đúng bằng số dư trước trừ đi số tiền chuyển và phí. Nó cũng phải kiểm lịch sử giao dịch xuất hiện đúng một dòng mới, không phải hai. Vì E2E chậm và đắt, ta chỉ giữ vài luồng quan trọng nhất ở tầng này và đẩy phần kiểm tổ hợp xuống tầng API.
// tests/e2e/transfer.spec.ts — oracle nghiệp vụ, không chỉ "toast success"
import { test, expect } from './seed.spec';
test('chuyển khoản giữ bất biến số dư và lịch sử', async ({ authedPage: page, sender, receiver }) => {
await page.goto('/wallet');
const before = Number((await page.getByTestId('balance').innerText()).replace(/\D/g, ''));
await page.getByRole('button', { name: 'Chuyển tiền' }).click();
await page.getByLabel('Người nhận').fill(receiver.email);
await page.getByLabel('Số tiền').fill('20000');
await page.getByRole('button', { name: 'Xác nhận' }).click();
await expect(page.getByRole('status')).toHaveText(/Thành công/);
// Oracle 1: số dư giảm ĐÚNG số tiền + phí (giả sử phí 0)
const after = Number((await page.getByTestId('balance').innerText()).replace(/\D/g, ''));
expect(after).toBe(before - 20000);
// Oracle 2: lịch sử có ĐÚNG 1 dòng mới cho giao dịch này
await expect(page.getByTestId('tx-row')).toHaveCount(1);
});
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!