Mục lục
- Mục Tiêu Bài Học
requestFixture Là Gì- Khác
page.context().request— Cookie Sharing - Cú Pháp Cơ Bản
- Methods Chính
- Options Chi Tiết
- Response Object
- Use Case: Pure API Test
- Use Case: API Setup & Teardown
- Use Case: Hybrid UI + API
- baseURL & extraHTTPHeaders
- TLS Client Certificates & Storage State
- Khác
fetch()Global - 4 Pitfalls
- Quiz + Bài Tiếp
Mục Tiêu Bài Học
- Hiểu
requestfixture làAPIRequestContext— layer HTTP client độc lập với browser, không đi qua rendering engine. - Phân biệt
requestfixture (không chia sẻ cookie với page) vớipage.context().request(chia sẻ cookie với BrowserContext). - Nắm đủ 7 methods:
get,post,put,patch,delete,head,fetchvà các options quan trọng. - Sử dụng response object:
ok(),status(),json(),text(),body(). - Áp dụng ba use case: pure API test, API setup/teardown, hybrid UI + API.
- Cấu hình
baseURLvàextraHTTPHeaderstrongplaywright.config.tsđể tái sử dụng. - Tránh 4 pitfall: cookie mismatch, quên await body, data trên GET, nhầm với route mock.
request Fixture Là Gì
request là built-in fixture của @playwright/test, kiểu APIRequestContext. Khi test destructure { request }, Playwright tạo một HTTP client riêng biệt — không đi qua Chromium/Firefox/WebKit, không render HTML, không thực thi JavaScript phía browser. Đây là HTTP transport thuần túy, giống một HTTP library như axios hay got nhưng tích hợp sẵn vào test runner.
Tính chất cốt lõi:
- Scope test: mỗi test nhận một instance mới — cookie jar, header defaults riêng biệt.
- Không phụ thuộc browser: không cần browser binary, test API thuần chạy nhanh hơn test UI đáng kể.
- Tích hợp config: nhận
baseURL,extraHTTPHeaders,ignoreHTTPSErrors,clientCertificatestừplaywright.config.ts. - Library mode: nếu không dùng Test Runner, tạo thủ công bằng
await playwright.request.newContext().
Vị trí trong bức tranh tổng thể:
requestfixture — HTTP client của test, không liên quan browser.page.context().request— HTTP client gắn với BrowserContext, chia sẻ cookie với UI session.page.route()— intercept request trình duyệt phát ra, dùng mock/modify. Bài Series Cơ Bản Nhóm 34 đã đề cập, không lặp lại ở đây.
Khác page.context().request — Cookie Sharing
Đây là điểm dễ nhầm nhất khi mới dùng request fixture:
| Khía cạnh | request fixture |
page.context().request |
|---|---|---|
| Cookie jar | Độc lập — KHÔNG chia sẻ với page UI | Chung với BrowserContext — chia sẻ với page |
| Khi dùng | Pure API test, setup/teardown không cần auth page | Gọi API với session đã login qua UI (ví dụ: lấy CSRF token) |
| Auth flow | Tự xử lý auth riêng (gọi login API, lưu token header) | Kế thừa auth từ page (đã login UI → cookie tự có) |
| Storage state | Có thể lưu/load riêng qua storageState() |
Dùng storage state của context |
Ví dụ minh hoạ sự khác biệt:
test('cookie difference', async ({ page, request }) => {
// Login qua UI → BrowserContext lưu cookie
await page.goto('/login');
await page.fill('#username', 'admin');
await page.fill('#password', 'secret');
await page.click('button[type=submit]');
// BrowserContext giờ có session cookie
// page.context().request kế thừa cookie từ BrowserContext → có auth
const resA = await page.context().request.get('/api/profile');
console.log(resA.status()); // 200 — authenticated
// request fixture KHÔNG có cookie từ page → không auth
const resB = await request.get('/api/profile');
console.log(resB.status()); // 401 — unauthenticated
});
Quy tắc chọn:
- Cần cookie từ UI session → dùng
page.context().request. - Pure API không cần UI cookie, hoặc tự quản lý auth qua header → dùng
requestfixture.
Cú Pháp Cơ Bản
Destructure request từ fixture params:
import { test, expect } from '@playwright/test';
test('create user via API', async ({ request }) => {
const response = await request.post('https://api.example.com/users', {
data: { name: 'John', email: '[email protected]' },
});
expect(response.ok()).toBeTruthy(); // status 200-299
const body = await response.json();
expect(body.id).toBeDefined();
});
Có thể kết hợp với page trong cùng một test (hybrid pattern):
test('hybrid test', async ({ request, page }) => {
// request: setup data qua API
// page: verify trên UI
});
Methods Chính
APIRequestContext cung cấp 7 methods, tất cả async và trả về Promise<APIResponse>:
| Method | HTTP verb | Ghi chú |
|---|---|---|
request.get(url, options?) |
GET | Truyền params cho query string |
request.post(url, options?) |
POST | Dùng data cho JSON body |
request.put(url, options?) |
PUT | Thường dùng thay toàn bộ resource |
request.patch(url, options?) |
PATCH | Update một phần resource |
request.delete(url, options?) |
DELETE | Xóa resource |
request.head(url, options?) |
HEAD | Chỉ lấy headers, không có body |
request.fetch(url, options?) |
Tùy method |
Generic — truyền method: 'OPTIONS' hoặc verb bất kỳ |
// GET với query params
const res = await request.get('/api/products', {
params: { category: 'books', page: '1' }, // ?category=books&page=1
});
// DELETE
await request.delete(`/api/users/${userId}`);
// Custom verb qua fetch()
const res2 = await request.fetch('/api/resource', {
method: 'OPTIONS',
headers: { 'Access-Control-Request-Method': 'POST' },
});
Options Chi Tiết
Tất cả methods nhận object options dùng chung:
| Option | Kiểu | Mô tả |
|---|---|---|
data |
object | string | Buffer |
JSON body — object tự serialize, set Content-Type: application/json |
form |
object |
URLEncoded body — set Content-Type: application/x-www-form-urlencoded |
multipart |
object |
Multipart/form-data — dùng khi upload file |
params |
object | URLSearchParams |
Query string — append vào URL. Hỗ trợ URLSearchParams từ v1.47 |
headers |
object |
Custom headers — merge với extraHTTPHeaders từ config |
failOnStatusCode |
boolean |
Throw error nếu status >= 400. Mặc định false |
ignoreHTTPSErrors |
boolean |
Bỏ qua lỗi TLS/SSL |
maxRedirects |
number |
Giới hạn số lần redirect. Từ v1.46 |
maxRetries |
number |
Retry khi network error (không phải 4xx/5xx). Từ v1.46 |
timeout |
number |
Timeout milliseconds. Mặc định dùng actionTimeout từ config |
Ví dụ upload file với multipart:
import { readFileSync } from 'fs';
const res = await request.post('/api/upload', {
multipart: {
file: {
name: 'report.pdf',
mimeType: 'application/pdf',
buffer: readFileSync('./fixtures/report.pdf'),
},
description: 'Monthly report',
},
});
Ví dụ với failOnStatusCode:
// Không có failOnStatusCode — phải check thủ công
const res = await request.get('/api/users/999');
if (!res.ok()) {
throw new Error(`Expected success, got ${res.status()}`);
}
// Có failOnStatusCode — test tự throw nếu status >= 400
const res2 = await request.get('/api/users/999', { failOnStatusCode: true });
// Nếu 404 → Playwright throw ngay, không cần check thủ công
Response Object
Methods của APIResponse:
| Method | Trả về | Ghi chú |
|---|---|---|
response.ok() |
boolean |
True khi status 200–299 |
response.status() |
number |
HTTP status code: 200, 201, 404, 500… |
response.statusText() |
string |
"OK", "Not Found", "Internal Server Error"… |
response.headers() |
object |
Header names lowercase: { 'content-type': 'application/json' } |
response.headersArray() |
Array<{name, value}> |
Khi cần header lặp (Set-Cookie nhiều lần) |
response.json() |
Promise<any> |
Parse JSON body — cần await |
response.text() |
Promise<string> |
Body dạng string — cần await |
response.body() |
Promise<Buffer> |
Raw bytes — dùng khi body là binary (image, PDF) |
response.url() |
string |
URL cuối sau redirect |
const res = await request.post('/api/users', {
data: { name: 'Alice' },
});
// Status
expect(res.status()).toBe(201);
expect(res.ok()).toBeTruthy();
// Body JSON — PHẢI await
const body = await res.json();
expect(body.id).toBeDefined();
expect(body.name).toBe('Alice');
// Header
expect(res.headers()['content-type']).toContain('application/json');
// URL cuối (sau redirect 301 → 200)
console.log(res.url());
Use Case: Pure API Test
Test backend API không cần UI. Phù hợp khi cần verify contract API (status, schema, header) mà không cần render browser.
import { test, expect } from '@playwright/test';
test('GET /api/users trả danh sách', async ({ request }) => {
const res = await request.get('/api/users');
expect(res.status()).toBe(200);
const users = await res.json();
expect(Array.isArray(users)).toBe(true);
expect(users.length).toBeGreaterThan(0);
expect(users[0]).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
email: expect.any(String),
});
});
test('POST /api/users tạo user mới', async ({ request }) => {
const res = await request.post('/api/users', {
data: { name: 'Bob', email: '[email protected]' },
});
expect(res.status()).toBe(201);
const created = await res.json();
expect(created.id).toBeDefined();
expect(created.name).toBe('Bob');
});
test('DELETE /api/users/:id xóa user', async ({ request }) => {
// Tạo trước để xóa
const createRes = await request.post('/api/users', {
data: { name: 'Temp', email: '[email protected]' },
});
const { id } = await createRes.json();
const deleteRes = await request.delete(`/api/users/${id}`);
expect(deleteRes.status()).toBe(204);
});
Pure API test chạy nhanh hơn UI test vì không mở browser. Phù hợp chạy trong CI gate đầu (fail fast nếu API broken trước khi chạy E2E đầy đủ).
Use Case: API Setup & Teardown
Tạo hoặc dọn dẹp test data qua API thay vì điền form UI. Nhanh hơn nhiều vì không cần render, navigate, fill, submit.
import { test, expect } from '@playwright/test';
let createdUserId: number;
test.beforeEach(async ({ request }) => {
// Setup: tạo user test data qua API — nhanh hơn điền form UI
const res = await request.post('/api/users', {
data: { name: 'Test User', email: `test_${Date.now()}@example.com` },
});
expect(res.ok()).toBeTruthy();
const user = await res.json();
createdUserId = user.id;
});
test.afterEach(async ({ request }) => {
// Teardown: cleanup sau test — đảm bảo không pollute DB
if (createdUserId) {
await request.delete(`/api/users/${createdUserId}`);
}
});
test('user profile page hiển thị đúng', async ({ page }) => {
await page.goto(`/users/${createdUserId}`);
await expect(page.getByText('Test User')).toBeVisible();
});
So với setup qua UI form:
- API: ~50ms cho 1 request POST.
- UI form: ~2-5 giây để navigate, fill nhiều field, submit, chờ redirect.
- Với 50 test, chênh lệch tích lũy lên tới vài phút mỗi lần chạy CI.
Use Case: Hybrid UI + API
Kết hợp request và page trong cùng một test: API tạo data nhanh, UI verify display.
test('user có thể xem order vừa tạo', async ({ request, page }) => {
// Setup: tạo order qua API (nhanh hơn UI form)
const res = await request.post('/api/orders', {
data: { productId: 1, quantity: 2 },
});
expect(res.ok()).toBeTruthy();
const order = await res.json();
// UI: navigate đến trang order detail
await page.goto(`/orders/${order.id}`);
await expect(page.getByText(`Order #${order.id}`)).toBeVisible();
await expect(page.getByText('Quantity: 2')).toBeVisible();
});
Pattern ngược lại — UI tạo, API verify:
test('form submit tạo record đúng trong DB', async ({ request, page }) => {
// UI: submit form
await page.goto('/products/new');
await page.fill('#name', 'Widget Pro');
await page.fill('#price', '99.99');
await page.click('button[type=submit]');
await page.waitForURL('/products/**');
// Lấy ID từ URL
const url = page.url();
const productId = url.split('/products/')[1];
// API: verify data thực sự lưu đúng
const res = await request.get(`/api/products/${productId}`);
const product = await res.json();
expect(product.name).toBe('Widget Pro');
expect(product.price).toBe(99.99);
});
Lưu ý: request fixture ở đây không có cookie từ page, nên nếu /api/products/:id yêu cầu auth thì phải dùng page.context().request.get(...) hoặc truyền header auth riêng cho request.
baseURL & extraHTTPHeaders
request fixture đọc cấu hình từ use block trong playwright.config.ts:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://api.example.com',
// Header áp dụng cho MỌI request — vd auth token
extraHTTPHeaders: {
'Authorization': `Bearer ${process.env.API_TOKEN}`,
'Accept': 'application/json',
},
},
});
Khi có baseURL, path tương đối trong test tự resolve thành full URL:
// Với baseURL = 'https://api.example.com'
await request.post('/users', { data: { name: 'John' } });
// → gọi https://api.example.com/users
await request.get('/users/123');
// → gọi https://api.example.com/users/123
Khi test cần override header riêng cho một request cụ thể:
// extraHTTPHeaders merge với headers per-request
// Nếu cùng key, per-request header thắng
const res = await request.get('/admin/stats', {
headers: {
'Authorization': 'Bearer ADMIN_TOKEN_OVERRIDE',
},
});
Cấu hình nhiều project với baseURL khác nhau (vd test env vs staging):
export default defineConfig({
projects: [
{
name: 'api-staging',
use: {
baseURL: 'https://staging-api.example.com',
extraHTTPHeaders: { 'X-Env': 'staging' },
},
},
{
name: 'api-prod',
use: {
baseURL: 'https://api.example.com',
extraHTTPHeaders: { 'X-Env': 'production' },
},
},
],
});
TLS Client Certificates & Storage State
TLS Client Certificates (v1.45)
Dành cho API yêu cầu mutual TLS authentication — server verify client certificate:
// playwright.config.ts
export default defineConfig({
use: {
clientCertificates: [{
origin: 'https://api.example.com',
certPath: './certs/client.pem',
keyPath: './certs/client-key.pem',
// passphrase: 'optional-passphrase',
}],
},
});
Certificate chỉ áp dụng cho request tới origin khớp. Nhiều origin có thể khai báo nhiều entry trong mảng.
Storage State
request.storageState() lưu cookies và localStorage của APIRequestContext ra file, cho phép reuse auth session:
// Setup: đăng nhập qua API, lưu session
const loginRes = await request.post('/api/auth/login', {
data: { username: 'admin', password: process.env.ADMIN_PASS },
});
expect(loginRes.ok()).toBeTruthy();
// Lưu cookies để dùng lại
await request.storageState({ path: 'playwright/.auth/api-admin.json' });
// Test sau load auth state thay vì login lại
// playwright.config.ts — project dùng auth đã lưu
{
name: 'api-authenticated',
use: {
storageState: 'playwright/.auth/api-admin.json',
},
}
Lưu ý: storage state lưu cookies của request fixture, KHÔNG phải cookie của page. Dùng page.context().storageState() để lưu cookie của browser context.
Khác fetch() Global
Node.js 18+ có fetch() global. Playwright test cũng có thể gọi fetch() trực tiếp, nhưng có sự khác biệt với request fixture:
| Khía cạnh | fetch() global |
request fixture |
|---|---|---|
| baseURL | Không — phải truyền full URL | Có — đọc từ playwright.config.ts |
| Cookie management | Không persist giữa calls (stateless) | Persist trong test scope |
| extraHTTPHeaders | Không áp dụng từ config | Tự áp dụng từ use.extraHTTPHeaders |
| TLS certificates | Không tích hợp | Đọc từ clientCertificates config |
| ignoreHTTPSErrors | Phải xử lý thủ công | Cấu hình qua option hoặc config |
| Tích hợp test timeout | Không | Tuân theo actionTimeout của Playwright |
Dùng fetch() global chỉ cho ad-hoc call không cần config chung. Khi cần consistency, tái sử dụng, và tích hợp với test config → dùng request fixture.
4 Pitfalls
Pitfall 1: Dùng request khi cần cookie từ page session
Test login qua UI, sau đó gọi API cần auth cookie. Dùng request fixture sẽ nhận 401 vì fixture không có cookie của BrowserContext.
// SAI — request fixture không có cookie từ page.goto('/login')
test('bad', async ({ page, request }) => {
await page.goto('/login');
await page.fill('#username', 'admin');
await page.click('[type=submit]');
// request không biết session cookie → 401
const res = await request.get('/api/me');
expect(res.status()).toBe(200); // FAIL
});
// ĐÚNG — page.context().request kế thừa cookie
test('good', async ({ page }) => {
await page.goto('/login');
await page.fill('#username', 'admin');
await page.click('[type=submit]');
const res = await page.context().request.get('/api/me');
expect(res.status()).toBe(200); // PASS
});
Pitfall 2: Quên await khi đọc body
response.json(), response.text(), response.body() đều async — không await trả về Promise, không phải giá trị.
// SAI — body là Promise, không phải object
const res = await request.get('/api/users/1');
const user = res.json(); // ← thiếu await
expect(user.name).toBe('Alice'); // FAIL — user là Promise object
// ĐÚNG
const user = await res.json();
expect(user.name).toBe('Alice');
Pitfall 3: Truyền data cho GET request
GET request không có body theo HTTP spec. Playwright bỏ qua data trên GET mà không báo lỗi — data bị mất hoàn toàn.
// SAI — data bị bỏ qua hoàn toàn
const res = await request.get('/api/search', {
data: { keyword: 'playwright' }, // ← ignored
});
// ĐÚNG — dùng params cho query string
const res = await request.get('/api/search', {
params: { keyword: 'playwright' }, // → /api/search?keyword=playwright
});
Pitfall 4: Nhầm request fixture với page.route
request fixture gọi HTTP API thật. page.route() intercept request của browser và có thể mock response. Đây là hai công cụ khác nhau cho mục đích khác nhau — không thể dùng request fixture để mock response cho page.
// request fixture — gọi API thật, không mock gì cả
test('gọi API thật', async ({ request }) => {
const res = await request.get('https://api.example.com/users');
// Đây là HTTP call thật tới server
});
// page.route — mock response trình duyệt (Series Cơ Bản Nhóm 34)
test('mock response browser', async ({ page }) => {
await page.route('/api/users', route => route.fulfill({
body: JSON.stringify([{ id: 1, name: 'Mocked' }]),
}));
await page.goto('/users');
// Browser nhận mock response, không gọi server thật
});
Quiz + Bài Tiếp
Quiz
Câu 1
Test cần login qua UI, sau đó gọi /api/profile cần session cookie vừa login. Nên dùng gì?
requestfixture với headerCookiethủ công.page.context().request— kế thừa cookie từ BrowserContext đã login.requestfixture — tự động có cookie vì cùng test.fetch()global với credentials: 'include'.
Đáp án
B. page.context().request chia sẻ cookie với BrowserContext. request fixture có cookie jar riêng biệt, không nhận cookie từ page.goto / page login flow. A hoạt động nhưng phức tạp và fragile. C sai — không tự động. D không có baseURL, extraHTTPHeaders từ config.
Câu 2
Đoạn code sau có lỗi gì?
test('check user', async ({ request }) => {
const res = await request.get('/api/users/1');
const user = res.json();
expect(user.name).toBe('Alice');
});
- Thiếu
baseURLtrong config. - Thiếu
awaittrướcres.json()—userlàPromise, không phải object. - Nên dùng
res.text()thay vìres.json(). - Không có lỗi.
Đáp án
B. Pitfall 2. response.json() là async method trả Promise<any>. Không await → user là Promise object, user.name là undefined, expect fail. Fix: const user = await res.json();.
Câu 3
Test cần lấy danh sách user với query ?role=admin&active=true. Cách nào đúng?
request.get('/api/users', { data: { role: 'admin', active: true } })request.get('/api/users?role=admin&active=true')request.get('/api/users', { params: { role: 'admin', active: 'true' } })- B và C đều đúng, A sai.
Đáp án
D. B (URL string đầy đủ) và C (dùng params option) đều cho cùng kết quả. C là cách được khuyến nghị vì tự escape ký tự đặc biệt và dễ đọc hơn. A sai vì data trên GET bị bỏ qua theo HTTP spec — Playwright không encode thành query string.
Câu 4
request fixture trong Test Runner khác playwright.request.newContext() (Library mode) ở điểm nào?
- Test Runner fixture tự inject và tự cleanup theo vòng đời test; Library mode phải tạo thủ công và tự gọi
dispose(). - Library mode hỗ trợ nhiều method hơn.
- Test Runner fixture không có
baseURL. - Hai cái hoàn toàn giống nhau, chỉ tên khác.
Đáp án
A. Đây là lợi thế cốt lõi của Test Runner. Fixture tự quản lý vòng đời — tạo khi test bắt đầu, dọn khi test kết thúc. Library mode cần await playwright.request.newContext() và sau khi xong phải await apiContext.dispose(), nếu quên thì context tồn tại suốt process. B sai (API như nhau). C sai (đọc từ config). D sai về cơ chế.
Câu 5
Sau test đăng nhập qua request.post('/api/auth/login', ...), muốn lưu session để test khác không cần đăng nhập lại, làm thế nào?
request.saveCookies('auth.json')await request.storageState({ path: 'playwright/.auth/api.json' })rồi cấu hình project dùngstorageStatenày.- Cookie tự động persist giữa các test — không cần làm gì.
- Dùng
page.context().storageState()sau API login.
Đáp án
B. request.storageState({ path }) lưu cookie jar của APIRequestContext ra file. Sau đó config project dùng use: { storageState: 'playwright/.auth/api.json' } để test khác khởi động với state đã auth. A — method không tồn tại. C sai — mỗi test nhận fresh instance. D sai — page.context().storageState() lưu cookie của BrowserContext, không phải request fixture.
Bài Tiếp Theo
Bài tiếp theo trong nhóm Fixtures Built-in đề cập fixture cuối cùng chưa được bài nào trước cover:
Bài 6: Built-in Fixture playwright — playwright fixture cung cấp instance Playwright object cấp cao nhất — cho phép truy cập playwright.chromium, playwright.firefox, playwright.webkit, playwright.devices, playwright.selectors trực tiếp trong test mà không cần import thêm. Use case: test cần tạo browser instance thủ công (library mode trong Test Runner), test device emulation nâng cao, custom selector engine.
Tài liệu tham khảo
- Playwright API — APIRequestContext
- Playwright API — APIResponse
- Playwright Docs — API testing
- Playwright Docs — API testing in Playwright Test
- Playwright Docs — Authentication
- Playwright Release Notes — v1.47 (params URLSearchParams)
- Playwright Release Notes — v1.46 (maxRedirects, maxRetries)
- Playwright Release Notes — v1.45 (TLS client certificates)
