Danh sách bài viết

Bài 5: Built-in Fixture `request` (APIRequestContext)

Bài 5 nhóm Fixtures Built-in. request là built-in fixture của Playwright Test Runner trả về APIRequestContext — đối tượng thực hiện HTTP request độc lập với browser, không đi qua page hay rendering engine. Khác page.context().request: request fixture không chia sẻ cookie với page UI (independent context), trong khi page.context().request chia sẻ cookie với BrowserContext đang chạy. Scope: fresh instance per test. Methods chính: get, post, put, patch, delete, head, fetch (generic). Options nhận: data (JSON body), form (URLEncoded), multipart, params (URLSearchParams, v1.47), headers, failOnStatusCode, timeout, maxRedirects/maxRetries (v1.46). Response object: ok(), status(), statusText(), headers(), json()/text()/body(), url(). Use case: pure API test, API setup/teardown nhanh hơn UI form, hybrid UI+API verify state. Config: baseURL trong playwright.config.ts, extraHTTPHeaders cho auth header chung, TLS client certificates (v1.45). request.storageState() save cookies sau API login để reuse. Khác fetch() global: có baseURL, cookie persist, interface nhất quán. 4 pitfall: dùng request thay page.context().request khi cần cookie share, quên await response.json(), truyền data cho GET, nhầm với page.route mock.

27/05/2026
15 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

  • Hiểu request fixture là APIRequestContext — layer HTTP client độc lập với browser, không đi qua rendering engine.
  • Phân biệt request fixture (không chia sẻ cookie với page) với page.context().request (chia sẻ cookie với BrowserContext).
  • Nắm đủ 7 methods: get, post, put, patch, delete, head, fetch và 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 baseURLextraHTTPHeaders trong playwright.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.
2

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, clientCertificates từ 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ể:

  • request fixture — 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.
3

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 request fixture.
4

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
});
5

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' },
});
6

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
7

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());
8

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 đủ).

9

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.
10

Use Case: Hybrid UI + API

Kết hợp requestpage 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.

11

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' },
      },
    },
  ],
});
12

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.

13

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.

14

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
});
15

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ì?

  1. request fixture với header Cookie thủ công.
  2. page.context().request — kế thừa cookie từ BrowserContext đã login.
  3. request fixture — tự động có cookie vì cùng test.
  4. 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');
});
  1. Thiếu baseURL trong config.
  2. Thiếu await trước res.json()userPromise, không phải object.
  3. Nên dùng res.text() thay vì res.json().
  4. 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.nameundefined, 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?

  1. request.get('/api/users', { data: { role: 'admin', active: true } })
  2. request.get('/api/users?role=admin&active=true')
  3. request.get('/api/users', { params: { role: 'admin', active: 'true' } })
  4. 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?

  1. 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().
  2. Library mode hỗ trợ nhiều method hơn.
  3. Test Runner fixture không có baseURL.
  4. 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?

  1. request.saveCookies('auth.json')
  2. await request.storageState({ path: 'playwright/.auth/api.json' }) rồi cấu hình project dùng storageState này.
  3. Cookie tự động persist giữa các test — không cần làm gì.
  4. 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 playwrightplaywright 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.