Danh sách bài viết

Bài 29: Pattern — API Client Fixture

Bài này đề cập một pattern cụ thể trong nhóm Custom Fixtures: wrap APIRequestContext thành một class với methods semantic — thay vì viết request.post('/users', { data }) rải rác, test chỉ gọi apiClient.createUser(data). Pattern giải quyết 4 vấn đề cùng lúc: DRY (tái sử dụng call logic), type-safety (method có signature rõ, autocomplete IDE), centralized error handling (throw trên non-2xx tại một chỗ), và mock-ability (thay class thật bằng class giả cho test offline). Bài cover cú pháp class APIClient, fixture wrap, test usage, extend với auth header (AuthedAPIClient), combine với testUser fixture qua dependency chain, testEnvironment helper, best practices phân tách domain, so sánh với request fixture low-level, limitations, và 4 pitfall.

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

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

  • Hiểu tại sao gọi request.post('/users', { data }) trực tiếp trong test gây vấn đề khi scale.
  • Viết class APIClient wrap APIRequestContext với methods semantic rõ ràng.
  • Tạo fixture apiClient inject class này vào test qua test.extend().
  • Extend class với auth header (AuthedAPIClient).
  • Combine apiClient với testUser fixture để tạo dependency chain cleanup tự động.
  • Xây dựng testEnv helper fixture cho seed data phức tạp.
  • Tránh 4 pitfall: mutable state, bỏ qua res.ok(), hardcode baseURL, hardcode token.
2

Vấn Đề Cần Giải Quyết

Giả sử project có 20 test cần tạo user trước khi chạy. Cách đơn giản nhất:

// test A
const res = await request.post('/users', {
  data: { email: '[email protected]', name: 'A' },
});
const user = await res.json();

// test B
const res = await request.post('/users', {
  data: { email: '[email protected]', name: 'B' },
});
const user = await res.json();

// ... 18 test nữa

Vấn đề phát sinh khi scale:

  • DRY violation: path /users, option shape { data }, xử lý response lặp lại ở mọi nơi. Khi API thay đổi (vd endpoint đổi thành /api/v2/users), cần sửa ở 20 chỗ.
  • Không type-safe: dataobject bất kỳ — IDE không nhắc thiếu field email hay name.
  • Error handling rải rác: mỗi test tự quyết định xử lý khi res.ok() === false — một số bỏ qua, một số throw, một số console.log.
  • Khó mock: test cần API server thật. Muốn chạy offline phải tìm và sửa từng chỗ gọi request.post.

Pattern API Client Fixture giải quyết cả 4 điểm này bằng cách trừu tượng hóa HTTP call vào một class, rồi inject class đó qua fixture.

3

Cú Pháp Class APIClient

File api-client.ts — class thuần TypeScript, không import gì từ @playwright/test ngoài type:

// api-client.ts
import type { APIRequestContext } from '@playwright/test';

export class APIClient {
  constructor(
    private request: APIRequestContext,
    private baseURL: string,
  ) {}

  async createUser(data: { email: string; name: string }) {
    const res = await this.request.post(`${this.baseURL}/users`, { data });
    if (!res.ok()) throw new Error(`Create user failed: ${res.status()}`);
    return await res.json();
  }

  async deleteUser(id: string) {
    const res = await this.request.delete(`${this.baseURL}/users/${id}`);
    if (!res.ok()) throw new Error(`Delete user failed: ${res.status()}`);
  }

  async listUsers() {
    const res = await this.request.get(`${this.baseURL}/users`);
    return await res.json();
  }
}

Điểm chú ý về thiết kế class:

  • APIRequestContext import dưới dạng type (chỉ type-level import) — không kéo theo runtime dependency không cần thiết.
  • baseURL nhận từ constructor, không hardcode trong class — cho phép tái cấu hình cho môi trường khác nhau.
  • Mỗi method kiểm tra res.ok() và throw rõ ràng với status code — lỗi sẽ bắn lên test dưới dạng exception có message mô tả, không phải silent fail.
  • Return res.json() thay vì APIResponse object — test nhận data trực tiếp, không cần await res.json() thêm lần nữa.
4

Fixture Wrap APIClient

File fixtures.ts — dùng test.extend() để tạo fixture apiClient:

// fixtures.ts
import { test as base } from '@playwright/test';
import { APIClient } from './api-client';

export const test = base.extend<{
  apiClient: APIClient;
}>({
  apiClient: async ({ request, baseURL }, use) => {
    const client = new APIClient(request, baseURL!);
    await use(client);
    // Không có cleanup đặc biệt ở đây —
    // request fixture tự cleanup bởi Playwright sau test
  },
});

export { expect } from '@playwright/test';

Fixture nhận hai built-in fixtures làm dependency: request (APIRequestContext) và baseURL (string từ config). Điều này đảm bảo APIClient hoạt động đồng nhất với cấu hình trong playwright.config.ts mà không cần truyền thủ công trong từng test.

Khai báo type APIClient trong generic của base.extend<{ apiClient: APIClient }> cho IDE biết kiểu trả về khi test destructure { apiClient }.

Trong playwright.config.ts, đảm bảo baseURL được khai báo:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
  },
});
5

Test Usage

Import testexpect từ fixtures.ts thay vì từ @playwright/test:

// user.spec.ts
import { test, expect } from './fixtures';

test('user có thể được tạo và xóa', async ({ apiClient }) => {
  const user = await apiClient.createUser({
    email: '[email protected]',
    name: 'Test',
  });
  expect(user.id).toBeDefined();
  await apiClient.deleteUser(user.id);
});

test('danh sách user không rỗng', async ({ apiClient }) => {
  const users = await apiClient.listUsers();
  expect(Array.isArray(users)).toBe(true);
});

Test chỉ biết createUser, deleteUser, listUsers — không biết gì về HTTP verb, path, hay response parsing. Nếu API đổi endpoint từ /users sang /api/v2/users, chỉ cần sửa trong APIClient class ở một chỗ.

6

4 Lợi Ích Cốt Lõi

DRY — Tái sử dụng call logic

HTTP path, method, body shape, response parsing đều nằm trong class. Mỗi test gọi một method, không lặp lại implementation detail. Khi API schema thay đổi, sửa một file duy nhất.

Type-safe với autocomplete

createUser nhận { email: string; name: string } — TypeScript báo lỗi compile-time nếu test truyền sai field:

// TypeScript báo lỗi: Property 'email' is missing
await apiClient.createUser({ name: 'Test' });

// IDE autocomplete hiện ra: email, name
await apiClient.createUser({ email: '...', name: '...' });

Return type cũng được infer từ method — user.id, user.email có autocomplete nếu khai báo return type explicit trong class.

Centralized error handling

Mỗi method kiểm tra res.ok() và throw kèm status code. Test không cần tự xử lý non-2xx — lỗi propagate tự nhiên như Exception trong test framework:

async createUser(data: { email: string; name: string }) {
  const res = await this.request.post(`${this.baseURL}/users`, { data });
  if (!res.ok()) throw new Error(`Create user failed: ${res.status()}`);
  return await res.json();
}
// Test fail với message rõ ràng: "Create user failed: 422"
// thay vì TypeError khi cố gọi .id trên undefined

Mock-able

Fixture inject class qua use(client) — có thể thay class thật bằng class giả mà không sửa test:

// mock-api-client.ts — cho test offline
export class MockAPIClient implements APIClient {
  async createUser(data) {
    return { id: 'mock-id-123', ...data };
  }
  async deleteUser(id) { /* no-op */ }
  async listUsers() { return []; }
}
7

Pattern Extend Với Auth

Khi API yêu cầu Bearer token, tạo subclass thay vì thêm auth logic vào APIClient gốc:

// api-client.ts (thêm vào file hoặc tách file riêng)
export class AuthedAPIClient extends APIClient {
  constructor(
    request: APIRequestContext,
    baseURL: string,
    private token: string,
  ) {
    super(request, baseURL);
  }

  async createUser(data: { email: string; name: string }) {
    const res = await this.request.post(`${this.baseURL}/users`, {
      data,
      headers: { Authorization: `Bearer ${this.token}` },
    });
    if (!res.ok()) throw new Error(`Create user failed: ${res.status()}`);
    return await res.json();
  }
}

Fixture cho AuthedAPIClient nhận token từ environment variable hoặc fixture khác (ví dụ authToken fixture):

// fixtures.ts — thêm fixture authedApiClient
export const test = base.extend<{
  apiClient: APIClient;
  authedApiClient: AuthedAPIClient;
}>({
  apiClient: async ({ request, baseURL }, use) => {
    await use(new APIClient(request, baseURL!));
  },

  authedApiClient: async ({ request, baseURL }, use) => {
    const token = process.env.API_TOKEN;
    if (!token) throw new Error('API_TOKEN env var is required');
    await use(new AuthedAPIClient(request, baseURL!, token));
  },
});

Khi token có expiry ngắn, lấy token động từ API login trong setup fixture thay vì đọc từ env:

authedApiClient: async ({ request, baseURL }, use) => {
  // Lấy token mới cho mỗi test — tránh expired token issue
  const loginRes = await request.post(`${baseURL}/auth/token`, {
    data: {
      clientId: process.env.CLIENT_ID,
      clientSecret: process.env.CLIENT_SECRET,
    },
  });
  if (!loginRes.ok()) throw new Error('Auth token fetch failed');
  const { token } = await loginRes.json();
  await use(new AuthedAPIClient(request, baseURL!, token));
},
8

Combine Với testUser Fixture — Dependency Chain

testUser fixture phụ thuộc vào apiClient fixture — đây là dependency chain: Playwright tự giải quyết thứ tự khởi tạo.

// fixtures.ts — thêm testUser
type Fixtures = {
  apiClient: APIClient;
  testUser: { id: string; email: string; name: string };
};

export const test = base.extend<Fixtures>({
  apiClient: async ({ request, baseURL }, use) => {
    await use(new APIClient(request, baseURL!));
  },

  testUser: async ({ apiClient }, use) => {
    // Setup: tạo user trước test
    const user = await apiClient.createUser({
      email: `test-${Date.now()}@x.com`,
      name: 'Test User',
    });
    // Truyền user xuống test body
    await use(user);
    // Teardown: xóa user sau test — chạy kể cả khi test fail
    await apiClient.deleteUser(user.id);
  },
});

Test dùng testUser fixture không cần tự create hay delete user:

test('profile page hiển thị đúng', async ({ page, testUser }) => {
  await page.goto(`/users/${testUser.id}`);
  await expect(page.getByText(testUser.name)).toBeVisible();
  await expect(page.getByText(testUser.email)).toBeVisible();
});

test('update user name', async ({ apiClient, testUser }) => {
  // testUser đã được tạo sẵn — test chỉ cần test logic update
  const updated = await apiClient.updateUser(testUser.id, { name: 'Updated' });
  expect(updated.name).toBe('Updated');
  // Teardown tự động — testUser fixture xóa user sau test
});

Cơ chế: khi testUser fixture được request, Playwright khởi tạo apiClient trước (nó là dependency), sau đó khởi tạo testUser dùng apiClient. Teardown chạy ngược: teardown testUser trước (xóa user), rồi teardown apiClient (cleanup request context). Thứ tự này được Playwright đảm bảo tự động.

9

Pattern testEnvironment Helper

Khi test cần nhiều loại data phức tạp (nhiều user, nhiều order, ...), tạo testEnv fixture cung cấp helper methods:

type TestEnv = {
  seedUsers: (count: number) => Promise<Array<{ id: string; email: string; name: string }>>;
};

export const test = base.extend<{
  apiClient: APIClient;
  testEnv: TestEnv;
}>({
  apiClient: async ({ request, baseURL }, use) => {
    await use(new APIClient(request, baseURL!));
  },

  testEnv: async ({ apiClient }, use) => {
    const created: string[] = []; // track IDs để cleanup

    const env: TestEnv = {
      async seedUsers(count: number) {
        const users = await Promise.all(
          Array(count).fill(0).map((_, i) =>
            apiClient.createUser({
              email: `seed-${i}-${Date.now()}@x.com`,
              name: `Seed User ${i}`,
            })
          )
        );
        created.push(...users.map((u) => u.id));
        return users;
      },
    };

    await use(env);

    // Cleanup tất cả user đã seed
    await Promise.all(created.map((id) => apiClient.deleteUser(id)));
  },
});

Test dùng testEnv để chuẩn bị data phức tạp:

test('bảng user hiển thị pagination đúng', async ({ page, testEnv }) => {
  // Seed 25 user để test pagination (mỗi trang 10 item)
  await testEnv.seedUsers(25);

  await page.goto('/admin/users');
  await expect(page.getByRole('row')).toHaveCount(11); // 10 row + 1 header
  await page.getByLabel('Next page').click();
  await expect(page.getByRole('row')).toHaveCount(11);
  // Cleanup 25 user được xử lý tự động sau test
});

Pattern này hữu ích hơn nhiều testUser đơn lẻ khi test feature yêu cầu volume data nhất định (pagination, sorting, search indexing).

10

Best Practices Phân Tách Domain

Khi project có nhiều domain (user, order, product, ...), không nhồi tất cả vào một class:

// user-api.ts
export class UserAPI {
  constructor(
    private request: APIRequestContext,
    private baseURL: string,
  ) {}
  async create(data: { email: string; name: string }) { /* ... */ }
  async delete(id: string) { /* ... */ }
  async list() { /* ... */ }
  async getById(id: string) { /* ... */ }
}

// order-api.ts
export class OrderAPI {
  constructor(
    private request: APIRequestContext,
    private baseURL: string,
  ) {}
  async create(data: { userId: string; productId: string; qty: number }) { /* ... */ }
  async cancel(id: string) { /* ... */ }
  async listByUser(userId: string) { /* ... */ }
}

// product-api.ts
export class ProductAPI {
  constructor(
    private request: APIRequestContext,
    private baseURL: string,
  ) {}
  async create(data: { name: string; price: number }) { /* ... */ }
  async delete(id: string) { /* ... */ }
}

Fixture aggregate nhiều domain client:

type APIClients = {
  users: UserAPI;
  orders: OrderAPI;
  products: ProductAPI;
};

export const test = base.extend<{ api: APIClients }>({
  api: async ({ request, baseURL }, use) => {
    const base = baseURL!;
    await use({
      users: new UserAPI(request, base),
      orders: new OrderAPI(request, base),
      products: new ProductAPI(request, base),
    });
  },
});

Test dùng api.users.create(), api.orders.cancel() — rõ ràng về domain, IDE autocomplete theo từng class.

Quy tắc đặt tên method:

  • Dùng createUser (hoặc users.create), không dùng postUsers — tên nên phản ánh business action, không phải HTTP verb.
  • Method đọc: getById, list, search.
  • Method ghi: create, update, delete, archive.
11

So Sánh Với request Fixture Low-level

Khía cạnh request fixture (low-level) API Client fixture (pattern này)
Abstraction HTTP transport thuần — verb, path, options Domain method — createUser, deleteOrder
Type safety dataobject bất kỳ Method signature có type cụ thể
Error handling Test tự quyết định — check res.ok() hay không Tập trung trong class — throw on non-2xx mặc định
Code duplication Path, body shape lặp ở mỗi test Một lần trong class, test gọi method
Mock Cần mock từng request.post call Thay cả class — một điểm swap
Raw response access Trực tiếp — res.headers(), res.status() Class ẩn response — cần expose thêm nếu cần header
Phù hợp khi Test API contract một-off, cần raw response detail Nhiều test cùng gọi một endpoint, project scale lớn

Hai cách không loại trừ nhau. Trong cùng một project, request fixture dùng cho test API contract chi tiết (verify header, status code cụ thể), còn API Client fixture dùng cho setup/teardown và business logic test.

12

Mock Pattern

Để test offline hoặc isolate test khỏi backend state, inject mock implementation thay vì real APIClient. TypeScript interface giúp đảm bảo mock implement đúng contract:

// api-client.interface.ts
export interface IAPIClient {
  createUser(data: { email: string; name: string }): Promise<{ id: string; email: string; name: string }>;
  deleteUser(id: string): Promise<void>;
  listUsers(): Promise<Array<{ id: string; email: string; name: string }>>;
}

// api-client.ts
export class APIClient implements IAPIClient {
  // ... implementation thật
}

// mock-api-client.ts
export class MockAPIClient implements IAPIClient {
  private users: Array<{ id: string; email: string; name: string }> = [];

  async createUser(data: { email: string; name: string }) {
    const user = { id: `mock-${Date.now()}`, ...data };
    this.users.push(user);
    return user;
  }
  async deleteUser(id: string) {
    this.users = this.users.filter((u) => u.id !== id);
  }
  async listUsers() {
    return [...this.users];
  }
}

Fixture mock — override fixture apiClient cho test offline:

// fixtures.offline.ts
import { test as base } from './fixtures'; // import test đã có apiClient
import { MockAPIClient } from './mock-api-client';

export const test = base.extend<{}>({
  // Override apiClient bằng mock implementation
  apiClient: async ({}, use) => {
    await use(new MockAPIClient() as any);
  },
});

Test offline import từ fixtures.offline.ts thay vì fixtures.ts — không cần sửa test code, chỉ đổi import.

Lưu ý: mock pattern này phù hợp cho test logic phụ thuộc vào data structure, không phù hợp cho test verify API integration thật. Tách rõ hai loại test file.

13

Limitations

Over-engineering cho project nhỏ

Nếu project chỉ có 5-10 test và 2-3 endpoint, viết class + fixture + interface + mock là overhead không tương xứng. request fixture dùng trực tiếp trong test là đủ. Pattern này có giá trị rõ ràng từ khoảng 20+ test hoặc khi cùng một endpoint được gọi từ 5+ chỗ.

Maintain type khi API schema thay đổi

Khi backend thêm field bắt buộc vào createUser (ví dụ thêm role: string), cần cập nhật:

  • Method signature trong APIClient class.
  • Interface IAPIClient nếu có.
  • Mọi test gọi apiClient.createUser(...) — TypeScript báo lỗi compile tại đây.
  • Mock class.

TypeScript error sẽ chỉ rõ nơi cần sửa, nhưng volume sửa vẫn nhiều hơn so với project không dùng pattern này (ở đó chỉ thêm field vào data object).

Test API contract cần raw response

Class ẩn APIResponse object — test không thể verify Location header, Set-Cookie, hay custom header của response. Cho test API contract cần verify detail như vậy, dùng request fixture trực tiếp.

14

4 Pitfalls

Pitfall 1: Class state mutable gây test isolation issue

Nếu APIClient lưu trạng thái trong property (ví dụ cache user list), các test dùng chung worker-scoped fixture sẽ thấy state từ test trước:

// SAI — class lưu state
class APIClient {
  private cachedUsers: any[] | null = null; // <-- state

  async listUsers() {
    if (this.cachedUsers) return this.cachedUsers; // test thứ 2 thấy cache cũ
    const res = await this.request.get(`${this.baseURL}/users`);
    this.cachedUsers = await res.json();
    return this.cachedUsers;
  }
}

Giữ class stateless — không lưu data từ response vào property. Nếu cần cache, scope nó trong test body (local variable), không trong class.

Pitfall 2: Quên check res.ok() trong một số method

Khi thêm method mới, dễ bỏ quên if (!res.ok()) throw. Method trả về kết quả của response lỗi dưới dạng "data hợp lệ":

// SAI — không check ok()
async getUser(id: string) {
  const res = await this.request.get(`${this.baseURL}/users/${id}`);
  return await res.json(); // Nếu 404, trả về { message: 'Not found' } — không phải user object
}

// Test vẫn pass vì object có field, nhưng không phải user thật
const user = await apiClient.getUser('nonexistent');
console.log(user.id); // undefined — bug bị che khuất

// ĐÚNG
async getUser(id: string) {
  const res = await this.request.get(`${this.baseURL}/users/${id}`);
  if (!res.ok()) throw new Error(`Get user failed: ${res.status()}`);
  return await res.json();
}

Pitfall 3: Hardcode baseURL trong class

// SAI — không reconfigurable
class APIClient {
  async createUser(data) {
    const res = await this.request.post('http://localhost:3000/users', { data });
    // ...
  }
}
// Khi chạy trên CI (port khác) hoặc staging — fail

Luôn nhận baseURL từ constructor, và fixture đọc baseURL từ Playwright config ({ request, baseURL }) thay vì hardcode.

Pitfall 4: Token hardcode trong constructor gây expire issue

// SAI — token hardcode hoặc đọc từ env một lần
class AuthedAPIClient extends APIClient {
  constructor(request, baseURL) {
    super(request, baseURL);
    this.token = 'hardcoded-token-abc'; // hoặc process.env.TOKEN
  }
}
// Token expire sau 1 giờ → test fail lúc chạy CI ban đêm

Lấy token động trong fixture setup (gọi login endpoint mỗi test run) thay vì hardcode. Nếu token có TTL dài, lấy một lần trong worker-scoped fixture để không login lại quá nhiều lần.

15

Quiz + Bài Tiếp

Quiz

Câu 1

Fixture apiClient nhận { request, baseURL } làm dependency. Điều này có nghĩa là gì khi chạy test?

  1. Playwright tạo APIClient một lần dùng chung toàn bộ test suite.
  2. Playwright inject requestbaseURL built-in fixtures vào fixture function trước khi tạo APIClient, đảm bảo baseURL từ config được dùng.
  3. Test phải tự truyền baseURL khi gọi method.
  4. Fixture yêu cầu baseURL được khai báo trực tiếp trong fixture file, không đọc từ config.
Đáp án

B. Playwright dependency injection tự động resolve built-in fixtures requestbaseURL trước khi chạy fixture function apiClient. baseURL có giá trị từ playwright.config.ts — fixture không cần biết config ở đâu. A sai — scope mặc định là test-level (mỗi test một instance). C sai — test chỉ destructure { apiClient }. D sai — đọc từ config thông qua built-in fixture baseURL.

Câu 2

Method createUser trong APIClient throw Error khi !res.ok(). Khi test gọi apiClient.createUser(data) và server trả 422, điều gì xảy ra?

  1. Test nhận object { message: 'Validation failed' } và tiếp tục chạy.
  2. Test fail với uncaught Error, message "Create user failed: 422". Playwright ghi lỗi vào report.
  3. apiClient fixture tự retry request tối đa 3 lần.
  4. Test bị skip, không fail.
Đáp án

B. Error throw từ fixture method propagate lên test body như exception thông thường. Nếu test không catch, Playwright mark test là FAILED với stack trace và message. Đây là mục tiêu của centralized error handling — lỗi bắn lên ngay với thông tin rõ ràng, không phải undefined field sau đó. A sai vì throw ngắt flow. C sai — không có auto-retry ở đây. D sai.

Câu 3

Fixture testUser depend vào apiClient. Playwright xử lý teardown theo thứ tự nào?

  1. Teardown apiClient trước, rồi testUser.
  2. Teardown testUser trước (xóa user), rồi apiClient.
  3. Teardown song song — không có thứ tự xác định.
  4. Chỉ teardown fixture được test destructure trực tiếp.
Đáp án

B. Playwright teardown theo thứ tự ngược setup: fixture được setup sau thì teardown trước. Vì testUser setup sau apiClient (nó depend vào apiClient), nên testUser teardown trước — đảm bảo user được xóa trong khi apiClient vẫn còn hoạt động. Nếu teardown apiClient trước thì testUser teardown sẽ không có client để gọi delete. A sai vì thứ tự ngược. C sai — Playwright đảm bảo thứ tự. D sai — dependency transitive đều được teardown.

Câu 4

Tại sao class APIClient nên stateless (không lưu data từ response vào property)?

  1. TypeScript không hỗ trợ property trong class dùng trong fixture.
  2. Class stateless giúp test isolation — mỗi lần gọi method nhận fresh data từ server, không bị ảnh hưởng bởi state từ call trước (cùng test hoặc test khác nếu worker-scoped).
  3. Class có state sẽ không compile được với TypeScript strict mode.
  4. Playwright fixture không hỗ trợ inject object có property.
Đáp án

B. Test isolation là lý do cốt lõi. Nếu class cache data, test A modify data, test B (chạy sau trong cùng worker) có thể nhận cache stale. Với worker-scoped fixture, vấn đề này nghiêm trọng hơn vì nhiều test share cùng instance. A, C, D đều sai — TypeScript và Playwright không có hạn chế này.

Câu 5

Khi nào không nên dùng API Client Fixture pattern?

  1. Khi project có hơn 50 test file.
  2. Khi cần verify Location header trong response của POST /users.
  3. Khi API yêu cầu Bearer token.
  4. Khi cần seed nhiều loại data trước test.
Đáp án

B. API Client fixture ẩn APIResponse object — test không truy cập được header của response. Để verify Location, Set-Cookie, hay custom header, dùng request fixture trực tiếp và giữ reference tới response object. A sai — với 50+ test là lúc pattern này phát huy tốt. C sai — có AuthedAPIClient pattern cho trường hợp này. D sai — testEnvironment helper fixture giải quyết trường hợp này.

Bài Tiếp Theo

Bài 30: Pattern — DB Seeder Fixture — pattern tạo fixture seed database trực tiếp qua connection (không qua HTTP API), dùng cho test cần data volume lớn hoặc data phức tạp hơn những gì public API cho phép tạo.