NỘI DUNG BÀI HỌC
🧠 Bản chất intercept network
🛠️ Cách thực hiện
🤝 Luồng hoạt động
🧠 Kết hợp giữa API - UI và waitForResponse
Phần 1: Intercept là gì? (Tư duy cốt lõi)
Hãy tưởng tượng bạn đang đi ăn nhà hàng.
-
Cách thông thường (Real API): Bạn gọi món Nhân viên báo bếp Bếp nấu thật Nhân viên mang ra. (Tốn thời gian, tốn nguyên liệu).
-
Intercept (Mock API): Bạn gọi món Nhân viên (là Playwright) chặn lại ngay tại bàn. Nhân viên rút từ trong túi ra một đĩa thức ăn nhựa (giống hệt thật) đặt lên bàn. Bếp không hề biết gì về việc này.
Trong kỹ thuật phần mềm:
Intercept là hành động Playwright đứng giữa Trình duyệt (Frontend) và Server (Backend). Nó chặn các Request lại và tự quyết định sẽ trả về cái gì (Response) mà không cần hỏi Server thật.
Phần 2: Tại sao phải dùng Intercept? (4 Lợi ích vàng)
Việc không gọi API thật mang lại 4 lợi ích sống còn cho Automation Test:
-
Tốc độ ánh sáng (Speed): Không chờ Server xử lý, không chờ mạng. Response trả về tức thì. Test chạy vèo vèo.
-
Dữ liệu sạch (Data Hygiene): Không tạo rác trong Database (User rác, đơn hàng rác). Không cần code đoạn dọn dẹp dữ liệu (Teardown).
-
Kiểm thử kịch bản khó (Edge Cases): Giả lập được những thứ Server thật rất khó làm như: Server sập (500), Mạng lag, Lỗi dữ liệu trùng (409).
-
Ổn định (Stability): Loại bỏ yếu tố "Test chập chờn" (Flaky tests) do mạng lag hay Server bảo trì.
Phần 3: Cách thực hiện trong Playwright (TypeScript)
Cấu trúc cơ bản nhất là dùng hàm page.route().
Cú pháp tổng quát:
await page.route('Đường_dẫn_API_muốn_chặn', async (route) => {
// Logic xử lý ở đây
});
Ví dụ 1: Mock trường hợp THÀNH CÔNG (Happy Path)
Giả lập API lấy danh sách sản phẩm.
test('Hiển thị danh sách sản phẩm từ Mock API', async ({ page }) => {
// 1. Thiết lập Intercept
await page.route('**/api/products', async (route) => {
// Trả về dữ liệu giả
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Cà phê Robusta', price: 50000 },
{ id: 2, name: 'Cà phê Arabica', price: 60000 }
])
});
});
// 2. Hành động (Frontend sẽ gọi API ở bước này)
await page.goto('/products');
// 3. Kiểm tra UI (Frontend lấy data giả hiển thị lên)
await expect(page.locator('.product-name').first()).toHaveText('Cà phê Robusta');
});
Ví dụ 2: Mock trường hợp LỖI (Error Handling)
Đây chính là ví dụ bạn đã hỏi. Giả lập email đã tồn tại.
test('Hiển thị lỗi khi Email đã đăng ký', async ({ page }) => {
const API_REGISTER = '**/api/auth/register';
// 1. Chặn và trả về lỗi 409
await page.route(API_REGISTER, async (route) => {
console.log('⛔ Đang giả lập lỗi 409...');
await route.fulfill({
status: 409, // Conflict
contentType: 'application/json',
body: JSON.stringify({
error: 'Email này đã được sử dụng, vui lòng chọn email khác.',
errorCode: 'USER_EXISTS'
})
});
});
// 2. Thao tác trên UI
await page.goto('/register');
await page.fill('#email', 'exists@email.com');
await page.click('#btn-submit');
// 3. Verify: Frontend phải đủ thông minh để bắt lỗi này và hiện text đỏ
await expect(page.locator('.error-message')).toHaveText('Email này đã được sử dụng, vui lòng chọn email khác.');
});
Phần 4: Cơ chế hoạt động & Trách nhiệm của Frontend
-
Playwright (Người ra đề): "Tao sẽ trả về lỗi 409 đấy, mày làm gì thì làm".
-
Frontend (Người giải đề): Code React/Vue phải có đoạn
try...catchhoặc.catch():-
Bắt lấy status
409. -
Đọc
body.error. -
setStateđể render dòng chữ báo lỗi ra màn hình.
-
-
Test Result:
-
Nếu UI hiện lỗi PASS (Frontend xử lý đúng logic).
-
Nếu UI im lìm hoặc loading mãi mãi FAIL (Frontend chưa handle lỗi này).
-
Phần 5: Một số lưu ý quan trọng (Best Practices)
-
Sử dụng Wildcard
**: API url thường thay đổi domain (ví dụdev.api.com,staging.api.com). Hãy dùng**/api/loginđể chặn endpoint này ở bất kỳ domain nào. -
Mock dữ liệu tối thiểu: Bạn không cần mock 100 trường trong User Object nếu UI chỉ hiển thị mỗi
username. Chỉ cần mock{ username: 'Test' }là đủ. -
Đừng lạm dụng: Vẫn nên có một vài bài test chạy với API thật (E2E Test) để đảm bảo Backend và Frontend thực sự kết nối được với nhau (Integration).
-
Tiếp tục Request (Continue): Đôi khi bạn chỉ muốn theo dõi request hoặc sửa header mà vẫn muốn nó đi đến server thật, hãy dùng
route.continue().
// Ví dụ: Thêm token vào header của mọi request đi ra
await page.route('**/*', async (route) => {
const headers = route.request().headers();
headers['Authorization'] = 'Bearer MY_SECRET_TOKEN';
await route.continue({ headers }); // Cho phép đi tiếp đến server thật
});
Cấu trúc tổng quát
Đây là một Object (Đối tượng) cấu hình phản hồi giả (Mock Response).
{
status: 200, // 1. Mã trạng thái HTTP
contentType: 'application/json', // 2. Định dạng dữ liệu
body: JSON.stringify(...) // 3. Nội dung dữ liệu (đã đóng gói)
}
Giải thích chi tiết từng dòng
1. status: 200
-
Syntax:
key: value -
Ý nghĩa: Đây là mã trạng thái HTTP (HTTP Status Code).
-
Tại sao là 200?: Số
200quy ước quốc tế là "OK" (Thành công). Nếu bạn muốn giả lập lỗi, bạn thay bằng404(Not Found),500(Server Error), v.v.
2. contentType: 'application/json'
-
Syntax: Chuỗi ký tự (String).
-
Ý nghĩa: Đây là cái nhãn dán lên gói hàng, báo cho trình duyệt biết: "Dữ liệu tao gửi về là dạng JSON nhé, đừng đọc nó như text thường hay ảnh".
-
Lưu ý: Nếu thiếu dòng này, đôi khi Frontend sẽ không hiểu dữ liệu và không render được.
3. body: JSON.stringify(...)
Đây là phần quan trọng nhất và hay gây nhầm lẫn nhất.
-
body: Là nội dung chính của gói tin. Trong giao thức HTTP, body khi truyền qua đường dây mạng phải là Chuỗi (String) hoặc Buffer (Byte). Nó không gửi được "Object Javascript" sống. -
JSON.stringify(...):-
Tác dụng: Hàm này nhận vào một Object/Array của Javascript và biến nó thành một Chuỗi văn bản (String format JSON).
-
Ví dụ: Nó biến
[{ id: 1 }](Object) thành"[{\"id\":1}]"(String).
-
4. Cấu trúc dữ liệu bên trong stringify
[ // Mở mảng (Array)
{ id: 1, name: '...' }, // Phần tử 1 (Object)
{ id: 2, name: '...' } // Phần tử 2 (Object)
] // Đóng mảng
-
[và](Ngoặc vuông): Biểu thị một Danh sách (Array). Vì API này trả về "Danh sách sản phẩm" nên phải dùng ngoặc vuông bao bên ngoài. -
{và}(Ngoặc nhọn): Biểu thị một Sản phẩm cụ thể (Object). -
Dấu phẩy
,: Dùng để ngăn cách giữa các dòng (key-value) hoặc giữa các phần tử trong mảng.
💡 MẸO CAO CẤP (Viết tắt cho gọn)
Playwright thông minh hơn bạn nghĩ. Thay vì phải viết dài dòng contentType rồi body: JSON.stringify(...), Playwright hỗ trợ cú pháp json.
Bạn có thể viết lại đoạn code trên ngắn gọn như sau (kết quả y hệt):
await route.fulfill({
status: 200,
// Dùng thẳng key 'json', Playwright tự động stringify và tự thêm header json cho bạn
json: [
{ id: 1, name: 'Cà phê Robusta', price: 50000 },
{ id: 2, name: 'Cà phê Arabica', price: 60000 }
]
});
Khuyên dùng: Cách dùng json: này gọn hơn, đỡ bị quên đóng ngoặc hoặc quên stringify.
Đây là một chủ đề chiến lược cực kỳ quan trọng. Nếu bạn nắm vững tư duy Hybrid Testing, bạn sẽ chuyển từ một người "biết viết code test" thành một người "biết thiết kế hệ thống test hiệu quả".
Dưới đây là phân tích sâu về tư duy này.
🧠Phần 6: TƯ DUY HYBRID TESTING (KIỂM THỬ LAI)
1. Bản chất cốt lõi: "Đi tắt đón đầu"
Trong kiểm thử phần mềm truyền thống (Pure UI Automation), chúng ta thường mô phỏng người dùng từ A đến Z.
-
Ví dụ: Muốn test chức năng "Thanh toán", ta làm: Mở web
Đăng ký
Login
Tìm hàng
Thêm giỏ
Thanh toán.
Vấn đề: 80% thời gian (Đăng ký, Login, Tìm hàng...) là lãng phí. Nếu Login bị lỗi, ta không thể test được Thanh toán.
Tư duy Hybrid: "Cái gì không phải trọng tâm bài test thì hãy dùng API để làm cho nhanh. Chỉ dùng UI cho đúng cái cần test."
2. Mô hình chiếc bánh kẹp (The Sandwich Model)
Tư duy Hybrid chia bài test làm 3 lớp, giống như một chiếc bánh sandwich:
🍞 Lớp 1: Setup (Chuẩn bị) - Dùng API (100%)
Đây là khâu tốn thời gian nhất nếu làm bằng tay.
-
Thay vì click click để tạo User, tạo Sản phẩm, tạo Giỏ hàng...
-
Hybrid: Gọi API
POST /login,POST /cart/add. -
Kết quả: Chỉ mất 0.5 giây để đưa hệ thống vào trạng thái sẵn sàng (Ready State), thay vì mất 2 phút click UI.
🥩 Lớp 2: Action (Hành động chính) - Dùng UI (100%)
Đây là "miếng thịt" - trọng tâm của bài test.
-
Vì bạn đang test giao diện/trải nghiệm người dùng, nên bước này bắt buộc dùng
page.click,page.fill. -
Ví dụ: Upload ảnh, Kéo thả, Click nút "Thanh toán", Nhìn thấy popup.
🍞 Lớp 3: Verification (Kiểm tra) - Dùng cả hai (UI + API)
-
Check UI: Để đảm bảo người dùng thấy đúng (Màu xanh, chữ "Thành công").
-
Check API/DB: Để đảm bảo dữ liệu lưu đúng (User ID đã được tạo trong database chưa).
3. Ví dụ so sánh: Bài toán "Kiểm tra Checkout"
Hãy xem sự khác biệt khủng khiếp về hiệu năng:
🐢 Cách cũ (Pure UI - Tuần tự)
-
Mở trình duyệt.
-
Click "Đăng ký"
Điền form
Submit (Mất 10s).
-
Click "Login"
Điền pass
Submit (Mất 5s).
-
Gõ ô tìm kiếm "iPhone"
Enter (Mất 3s).
-
Click vào sản phẩm
Chờ load (Mất 2s).
-
Click "Thêm vào giỏ" (Mất 1s).
-
Vào giỏ hàng
Click "Thanh toán" (Mất 2s).
-
Test logic thanh toán (Trọng tâm).
Tổng: ~30 giây + Rủi ro cao (Chết ở bước Đăng ký là khỏi test Thanh toán).
⚡ Cách Hybrid (API + UI)
-
API:
request.post('/api/register')(Tạo user ngầm). -
API:
request.post('/api/login')Lấy Token nạp vào Browser Context (Bỏ qua màn hình login).
-
API:
request.post('/api/cart', { product: 'iPhone' })(Bỏ qua bước tìm kiếm). -
UI:
page.goto('/checkout')(Bay thẳng vào trang thanh toán). -
Test logic thanh toán.
Tổng: ~3 giây + Ổn định tuyệt đối.
4. Quy tắc 80/20 trong Hybrid Testing
Khi nào dùng API, khi nào dùng UI? Hãy nhớ quy tắc này:
-
Dùng API (Shortcut) cho:
-
Tạo dữ liệu đầu vào (Pre-condition).
-
Dọn dẹp dữ liệu sau khi test (Teardown/Delete).
-
Các bước Đăng nhập/Đăng ký (trừ khi bạn đang test chính chức năng Login).
-
Các bước trung gian (Thêm hàng vào giỏ, duyệt đơn hàng).
-
-
Dùng UI (Interaction) cho:
-
Tính năng chính đang cần test (Feature under test).
-
Các thao tác người dùng phức tạp (Kéo thả, Canvas, Vẽ).
-
Kiểm tra hiển thị (Visual Regression).
-
Luồng người dùng cuối (End-to-End User Journey).
-
5. Kết luận
Tư duy Hybrid Testing không chỉ là kỹ thuật, nó là sự thực dụng.
-
Bạn không được trả lương để viết những bài test chạy chậm như rùa bò.
-
Bạn được trả lương để viết những bài test Nhanh, Ổn định và Tìm ra lỗi.
-
Hybrid Testing chính là vũ khí giúp bạn đạt được điều đó.
🕵️ Phần 7: KỸ THUẬT waitForResponse
1. Nó là gì? (Định nghĩa)
Hãy tưởng tượng Playwright là một người giám sát.
-
page.click(): Là hành động chủ động (ra lệnh cho trình duyệt làm gì đó). -
page.waitForResponse(): Là hành động thụ động (ngồi im và quan sát).
Định nghĩa:
waitForResponselà lệnh tạm dừng bài test lại, "ngồi rình" ở cửa ngõ mạng (Network) của trình duyệt, và chỉ cho phép bài test chạy tiếp khi bắt được gói tin phản hồi (Response) từ Server thỏa mãn điều kiện bạn đặt ra.
Khác biệt với intercept:
-
intercept(page.route): Chặn xe, đổi hàng, trả về hàng giả. (Can thiệp) -
waitForResponse: Chỉ đứng nhìn xe đi qua, kiểm tra biển số, nếu đúng xe mình cần thì báo "OK, đi tiếp". (Quan sát)
2. Ứng dụng thực tế (Khi nào thì dùng?)
Bạn chỉ dùng nó trong 3 trường hợp "bất khả kháng" mà việc check UI (expect(locator)) không làm được hoặc làm không tốt:
✅ Case 1: Xử lý độ trễ Server cực lâu
Khi bạn upload file và Server mất tới 60 giây để xử lý.
-
Nếu dùng UI
expect: Bạn không biết chính xác khi nào nó xong để chờ. -
Dùng
waitForResponse: Bạn biết chính xác 100% giây phút Server trả lời "Done". Test chạy tiếp ngay lập tức, không lãng phí 1 giây nào.
✅ Case 2: Kiểm tra dữ liệu ngầm (Data Validation)
UI hiển thị "Tạo thành công", nhưng bạn muốn chắc chắn Server đã lưu đúng tên là "Nguyễn Văn A" chứ không phải "Null".
-
waitForResponsegiúp bạn tóm lấy cục JSON trả về để soi dữ liệu bên trong (response.json()).
✅ Case 3: Hành động không có UI (Silent Action)
Ví dụ tính năng Auto-Save (Tự động lưu). Khi nó chạy, giao diện không thay đổi gì cả. Làm sao test biết nó đã chạy?
-
Phải dùng
waitForResponseđể bắt gói tin/api/autosavetrả về 200 OK.
3. Giải phẫu Syntax (Cú pháp chi tiết)
Cú pháp chuẩn của nó như sau:
const response = await page.waitForResponse(predicate, options);
Chúng ta có 2 tham số cần quan tâm: predicate (Bộ lọc) và options (Cấu hình).
A. Tham số 1: predicate (Bộ lọc - Quan trọng nhất)
Đây là cái "lưới" để bạn lọc ra đúng gói tin mình cần giữa hàng trăm gói tin (ảnh, css, js) đang bay qua lại.
Có 3 cách viết predicate, nhưng Cách 3 là chuẩn nhất:
-
Cách 1 (Dùng URL String - Sơ sài):
'**/api/upload' // Chỉ check URL. Lỡ API trả về lỗi 500 nó vẫn bắt -> Test sai. -
Cách 2 (Dùng Regex - Khó đọc):
/\/api\/upload/ -
Cách 3 (Dùng Function - Chuẩn Senior ⭐️):
Check kỹ cả URL lẫn Status code.
// "res" đại diện cho gói tin vừa bay qua (res) => { return res.url().includes('/api/upload') // 1. Đúng đường dẫn && res.status() === 200; // 2. Server báo thành công }
B. Tham số 2: options (Cấu hình thời gian)
Đây là nơi bạn quy định độ kiên nhẫn.
{
timeout: 70000 // Đơn vị mili-giây (70 giây)
}
-
Mặc định: 30,000ms (30 giây).
-
Lưu ý: Nếu server xử lý mất 60s, bạn BẮT BUỘC phải set timeout > 60s ở đây. Nếu không lệnh này sẽ báo lỗi timeout trước khi server kịp trả lời.
4. Cách ghép nối vào bài Test (Pattern chuẩn)
Đây là phần quan trọng nhất. Nếu viết sai thứ tự, lệnh này vô dụng.
Quy tắc vàng: Phải giăng lưới (wait) TRƯỚC hoặc CÙNG LÚC với việc ném đá (action).
Cấu trúc Code: Promise.all
// Sử dụng Promise.all để chạy song song 2 luồng
const [response] = await Promise.all([
// LUỒNG 1: Ngồi canh me (Giăng lưới)
page.waitForResponse(
(res) => res.url().includes('/api/upload') && res.status() === 200,
{ timeout: 70000 } // Chờ tối đa 70s
),
// LUỒNG 2: Thực hiện hành động để kích hoạt API (Ném đá)
page.click('#btn-upload-file')
]);
// Sau khi dòng trên chạy xong, nghĩa là API đã về và thành công.
// Giờ bạn có thể lấy data ra check.
const data = await response.json();
console.log(data);
Tóm tắt dễ nhớ:
-
Nó là gì? Lính gác cổng mạng.
-
Dùng khi nào? Khi API chậm, khi cần soi data JSON, hoặc khi UI không đổi.
-
Syntax: Luôn dùng Function để filter (check cả URL + Status).
-
Triển khai: Luôn bọc trong
Promise.allcùng với hành động click.
