Danh sách bài viết

Bài 119: Modify Response Từ Real Backend

Khi cần test edge case mà dữ liệu sát production — thay vì mock toàn bộ response, bạn có thể fetch response thật từ backend rồi chỉ patch những field cần thiết trước khi trả về browser. Pattern này giữ nguyên phần lớn data thật (schema, field còn lại, header, status), giảm chi phí maintain so với full mock. Bài này trình bày cú pháp, các use case thực tế, và những pitfall cụ thể cần tránh.

28/05/2026
0 lượt xem
1

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

  • Phân biệt rõ "modify real" và "full mock" — khi nào mỗi cách phù hợp hơn.
  • Nắm cú pháp route.fetch() → patch JSON → route.fulfill({ response, json }).
  • Hiểu cơ chế fulfill({ response, json }): response làm baseline cho status/headers, json override body.
  • Viết được các pattern thực tế: inject empty state, inject error field, patch array, conditional modify, header override.
  • Liệt kê limitation của pattern modify real (backend phải accessible, schema thay đổi cần update patch).
  • Tránh 4 pitfall: quên truyền response vào fulfill, patch field không tồn tại, backend down không có fallback, mutate nested object gây side-effect.
2

Modify Real vs Full Mock

Có hai cách để kiểm soát response trong Playwright route handler:

Full Mock

Dùng route.fulfill({ json: {...} }) với data tự soạn hoàn toàn. Không gọi server. Kiểm soát tuyệt đối response — phù hợp khi test cần isolation hoặc backend chưa có.

// Full mock — data hardcode, không gọi server
await page.route('**/api/user', (route) => {
  route.fulfill({
    json: {
      id: 1,
      name: 'Test User',
      subscription: 'premium',
      featureFlags: { newDashboard: true },
    },
  });
});

Nhược điểm của full mock: phải tự soạn đầy đủ toàn bộ schema. Khi backend thêm field mới hoặc đổi cấu trúc, mock data cần cập nhật thủ công — tốn công maintain, dễ lỗi drift giữa mock và production.

Modify Real

Fetch response thật từ backend → patch một vài field → fulfill về browser. Giữ nguyên phần lớn data production, chỉ override đúng field cần thiết cho test.

// Modify real — fetch thật, patch 2 field, giữ nguyên phần còn lại
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();          // gọi backend thật
  const json = await response.json();

  json.subscription = 'premium';                 // patch field test
  json.featureFlags.newDashboard = true;

  await route.fulfill({ response, json });        // trả về browser với data modified
});

Ưu điểm: data structure khớp backend thật. Các field không patch giữ nguyên giá trị production — test hoạt động với context gần thực tế hơn. Khi schema backend thêm field mới, không cần cập nhật gì nếu không cần patch field đó.

Tiêu chí Full Mock Modify Real
Gọi backend? Không
Data sát production? Không (tự soạn) Có (backend trả)
Maintain khi schema đổi? Phải cập nhật toàn bộ mock Chỉ cập nhật field patch
Kiểm soát response? Tuyệt đối Chỉ field được patch
Phụ thuộc backend? Không Có — backend phải accessible
Phù hợp khi? Test isolation, backend chưa có, endpoint phức tạp Test edge case, feature flag, A/B — cần data sát thật
3

Cú Pháp Cơ Bản

Ba bước: fetch response thật → parse JSON → patch field → fulfill.

await page.route('**/api/user', async (route) => {
  // Bước 1: fetch response thật từ backend
  const response = await route.fetch();

  // Bước 2: parse JSON
  const json = await response.json();

  // Bước 3: patch field cần test
  json.subscription = 'premium';
  json.featureFlags.newDashboard = true;

  // Bước 4: fulfill — giữ status/headers từ response thật, override body
  await route.fulfill({ response, json });
});

Trong test đầy đủ:

import { test, expect } from '@playwright/test';

test('premium user sees new dashboard', async ({ page }) => {
  await page.route('**/api/user', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    json.subscription = 'premium';
    json.featureFlags.newDashboard = true;

    await route.fulfill({ response, json });
  });

  await page.goto('/dashboard');
  await expect(page.getByTestId('new-dashboard')).toBeVisible();
});

Điểm quan trọng: handler phải là async vì cả route.fetch()response.json() đều trả Promise. Quên async sẽ gây lỗi runtime.

4

fulfill({ response, json }) — Cơ Chế Override

route.fulfill có ba dạng dùng khi modify real:

{ response } — Pass-through giữ nguyên

Dùng status, headers, body từ response thật. Không override gì.

await route.fulfill({ response });
// Giống route.continue() về kết quả, nhưng đi qua test để inspect

{ response, json } — Override body giữ status/headers

Status và headers lấy từ response thật. Body được thay bằng json (Playwright tự stringify và set content-type: application/json).

const json = await response.json();
json.role = 'admin';

await route.fulfill({ response, json });
// → status: như server trả (vd 200)
// → headers: như server trả (cache-control, set-cookie, custom headers)
// → body: JSON.stringify(json) với field đã patch

{ response, body } — Override body dạng string/Buffer

Tương tự json nhưng dùng khi body không phải JSON (text, HTML, binary) hoặc khi cần kiểm soát định dạng serialize thủ công.

const text = await response.text();
const patched = text.replace('free_tier', 'premium_tier');

await route.fulfill({ response, body: patched });

Tóm tắt priority

Khi truyền cả response lẫn field override, field override thắng:

  • response: baseline — status, headers, body từ backend.
  • json hoặc body: override body, tự set content-type nếu dùng json.
  • status: override status code.
  • headers: override headers (không merge — thay hẳn).
// Override status + thêm header + patch body
await route.fulfill({
  response,                                          // baseline
  status: 200,                                       // override status
  headers: {
    ...response.headers(),                           // giữ headers gốc
    'x-test-patched': 'true',                        // thêm header test
  },
  json: patchedData,                                 // override body
});
5

Use Case 1 — Test Edge Case Với Subscription Tier

Test cần verify UI khi user có tier "premium" nhưng không muốn seed user premium vào DB test. Fetch user thật từ backend rồi patch field subscription.

test('premium badge shows on profile', async ({ page }) => {
  await page.route('**/api/user', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    // Patch tier để test premium UI — giữ nguyên id, name, avatar, etc.
    json.subscription = 'premium';
    json.subscriptionExpiresAt = '2027-01-01';

    await route.fulfill({ response, json });
  });

  await page.goto('/profile');
  await expect(page.getByTestId('premium-badge')).toBeVisible();
  await expect(page.getByText('Premium')).toBeVisible();
});

Lợi ích so với full mock: không cần soạn lại toàn bộ user object. Các field như id, name, avatar, createdAt giữ nguyên từ backend — nếu UI hiển thị những field này, test vẫn hoạt động đúng.

6

Use Case 2 — Feature Flag Test

Backend trả config object với feature flags. Test cần bật flag newDashboard để verify UI mới — không cần deploy backend hay thay đổi config production.

test('new dashboard renders when flag enabled', async ({ page }) => {
  await page.route('**/api/user', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    // Patch nested object feature flags — giữ các flag khác
    json.featureFlags = {
      ...json.featureFlags,              // giữ nguyên flags hiện có
      newDashboard: true,                // bật flag cần test
    };

    await route.fulfill({ response, json });
  });

  await page.goto('/');
  await expect(page.getByTestId('new-dashboard-widget')).toBeVisible();
});

test('old dashboard when flag disabled', async ({ page }) => {
  await page.route('**/api/user', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    json.featureFlags = {
      ...json.featureFlags,
      newDashboard: false,               // tắt flag
    };

    await route.fulfill({ response, json });
  });

  await page.goto('/');
  await expect(page.getByTestId('classic-dashboard')).toBeVisible();
});

Pattern { ...json.featureFlags, newDashboard: true } giữ các flag khác nguyên — quan trọng vì UI có thể phụ thuộc nhiều flag cùng lúc. Nếu gán thẳng json.featureFlags = { newDashboard: true }, tất cả flag còn lại mất → test có thể fail vì reason không liên quan.

7

Use Case 3 — Inject Empty State

Test UI khi list rỗng — trường hợp thường khó reproduce vì DB test có sẵn data. Fetch response thật rồi override body thành empty array.

test('empty orders page shows empty state message', async ({ page }) => {
  await page.route('**/api/orders', async (route) => {
    const response = await route.fetch();

    // Override body thành list rỗng — giữ status 200 và headers từ response thật
    await route.fulfill({ response, json: [] });
  });

  await page.goto('/orders');
  await expect(page.getByText('Bạn chưa có đơn hàng nào')).toBeVisible();
  await expect(page.getByTestId('empty-state-illustration')).toBeVisible();
});

Không cần parse body trước — bỏ qua response.json() vì không cần inspect body cũ. Truyền thẳng json: [] để override. Status 200 và headers giữ nguyên từ backend — đảm bảo app code không nhầm thành lỗi.

Tương tự cho object rỗng:

await page.route('**/api/notifications/unread', async (route) => {
  const response = await route.fetch();
  // Test UI khi không có notification nào
  await route.fulfill({ response, json: { items: [], total: 0 } });
});
8

Use Case 4 — Inject Error In Field

Simulate trạng thái lỗi ở field cụ thể — status HTTP vẫn 200 (backend xử lý thành công) nhưng payload chứa error state trong field. Phổ biến với payment, verification, third-party integration.

test('card declined error UI renders correctly', async ({ page }) => {
  await page.route('**/api/payment', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    // Patch trạng thái lỗi — giữ nguyên các field khác (transactionId, amount, etc.)
    json.status = 'failed';
    json.errorCode = 'CARD_DECLINED';
    json.errorMessage = 'Thẻ bị từ chối. Vui lòng kiểm tra lại.';

    await route.fulfill({ response, json });
  });

  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Thanh toán' }).click();
  await expect(page.getByTestId('payment-error')).toBeVisible();
  await expect(page.getByText('CARD_DECLINED')).toBeVisible();
});

Pattern này test khả năng xử lý lỗi của UI mà không cần backend thật trả lỗi — backend vẫn trả 200 với data hợp lệ, test chỉ patch field statuserrorCode. Dùng để test resilience của app với các error state hiếm gặp.

Inject Missing Field — Test Default Behavior

Xóa field để test app xử lý missing data:

test('avatar fallback when no avatar field', async ({ page }) => {
  await page.route('**/api/user', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    delete json.avatarUrl;                           // xóa field để test fallback

    await route.fulfill({ response, json });
  });

  await page.goto('/profile');
  await expect(page.getByAltText('Default avatar')).toBeVisible();
});
9

Use Case 5 — A/B Variant

Test app theo từng variant A/B mà không phụ thuộc backend assign variant. Force variant qua patch response.

test('variant B shows new CTA button text', async ({ page }) => {
  await page.route('**/api/experiment/config', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    // Force variant B — giữ nguyên các experiment config khác
    json.experiments = {
      ...json.experiments,
      ctaButtonText: 'variant_b',         // force variant cần test
    };

    await route.fulfill({ response, json });
  });

  await page.goto('/landing');
  await expect(page.getByRole('button', { name: 'Bắt đầu miễn phí' })).toBeVisible();
});

test('variant A shows original CTA button text', async ({ page }) => {
  await page.route('**/api/experiment/config', async (route) => {
    const response = await route.fetch();
    const json = await response.json();

    json.experiments = {
      ...json.experiments,
      ctaButtonText: 'variant_a',
    };

    await route.fulfill({ response, json });
  });

  await page.goto('/landing');
  await expect(page.getByRole('button', { name: 'Đăng ký ngay' })).toBeVisible();
});

Pattern này loại bỏ sự không ổn định khi test A/B — không còn phụ thuộc vào logic phân chia random của backend. Mỗi test kiểm soát chính xác variant nào được hiển thị.

10

Patch Array Items

Khi response là array hoặc chứa array, dùng .map() để patch mỗi item — tránh mutate trực tiếp từng phần tử trong loop.

// Patch tất cả items — thêm discount cho mọi sản phẩm
await page.route('**/api/products', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  json.items = json.items.map((item: { id: number; price: number }) => ({
    ...item,                               // giữ nguyên các field gốc
    discount: 50,                          // thêm field discount
    discountedPrice: item.price * 0.5,
  }));

  await route.fulfill({ response, json });
});

Patch item đầu tiên — test UI với sản phẩm hết hàng:

await page.route('**/api/products', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  // Chỉ patch item đầu để test out-of-stock UI
  if (json.items && json.items.length > 0) {
    json.items = [
      { ...json.items[0], outOfStock: true, stock: 0 },  // patch item đầu
      ...json.items.slice(1),                               // giữ nguyên phần còn lại
    ];
  }

  await route.fulfill({ response, json });
});

Pattern { ...item, newField: value } (spread rồi override) tạo object mới, không mutate item gốc trong array. Điều này quan trọng khi một route handler bị đăng ký nhiều lần (vd test với page.route kết hợp context.route) — tránh side-effect cross-handler.

11

Conditional Modify

Patch chỉ khi data thỏa điều kiện — tránh lỗi nếu response không có data mong đợi.

await page.route('**/api/products', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  // Chỉ patch khi có sản phẩm — không lỗi khi list rỗng
  if (json.products && json.products.length > 0) {
    json.products[0].outOfStock = true;       // test out-of-stock UI cho item đầu
  }

  await route.fulfill({ response, json });
});

Conditional dựa trên giá trị field:

await page.route('**/api/orders', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  // Inject trạng thái delay cho order đang xử lý
  json.orders = json.orders.map((order: { status: string; id: string }) => {
    if (order.status === 'processing') {
      return { ...order, estimatedDelay: '2 ngày', delayReason: 'WAREHOUSE_ISSUE' };
    }
    return order;                             // giữ nguyên order status khác
  });

  await route.fulfill({ response, json });
});

Kiểm tra json.products && json.products.length > 0 trước khi patch là bắt buộc — backend có thể trả list rỗng hoặc field không tồn tại trong một số điều kiện. Thiếu guard → lỗi runtime Cannot read properties of undefined.

12

Header Override

Ngoài body, đôi khi cần patch header — vd disable cache để test không bị stale, hay thêm custom header để debug.

await page.route('**/api/products', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  json.items[0].featured = true;

  await route.fulfill({
    response,
    json,
    headers: {
      ...response.headers(),               // giữ nguyên headers gốc
      'cache-control': 'no-cache',         // disable cache cho test
      'x-test-modified': 'true',           // marker debug
    },
  });
});

Lưu ý: headers trong fulfill là replace — không merge với headers từ response. Phải dùng ...response.headers() để giữ lại headers gốc rồi thêm/override field cần thiết. Nếu bỏ spread, tất cả headers như content-type, set-cookie, authorization từ backend đều mất.

Trường hợp chỉ muốn override header, không cần patch body:

await page.route('**/api/config', async (route) => {
  const response = await route.fetch();

  await route.fulfill({
    response,
    headers: {
      ...response.headers(),
      'x-ratelimit-remaining': '0',        // simulate rate limit header
    },
  });
});
13

Limitation Của Pattern Modify Real

  • Backend phải accessible. Test không chạy được khi backend down hoặc không có network (CI offline). Full mock không có hạn chế này. Cần fallback handler hoặc cân nhắc dùng full mock cho test isolation.
  • Backend slow ảnh hưởng test. route.fetch() chờ backend trả response thật — backend có latency cao thì test chậm theo. Không kiểm soát được như full mock (fulfill ngay lập tức).
  • Schema thay đổi cần cập nhật patch logic. Nếu backend đổi tên field (vd subscriptionplan), patch vào field cũ không có tác dụng — test fail hoặc silent wrong. Cần review patch logic khi backend release schema mới.
  • Không phù hợp với endpoint có side-effect. route.fetch() gọi backend thật — POST/PUT/DELETE sẽ tạo/sửa/xóa data thật trong DB test. Với endpoint không idempotent, nên dùng full mock để tránh side-effect tích lũy qua nhiều lần chạy test.
14

4 Pitfalls

Pitfall 1 — Quên Truyền response Vào fulfill

Bỏ qua response trong fulfill({ json }) → mất status và headers từ backend thật. Playwright sẽ dùng status 200 mặc định và headers tối thiểu. Nếu app phụ thuộc header như set-cookie, cache-control, hay custom header auth, behavior sẽ sai.

// SAI — mất status và headers thật
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.role = 'admin';
  await route.fulfill({ json });              // thiếu response!
});

// ĐÚNG — giữ status và headers thật
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.role = 'admin';
  await route.fulfill({ response, json });   // truyền response làm baseline
});

Pitfall 2 — Patch Field Không Tồn Tại → undefined

Gán vào nested field khi parent object chưa tồn tại → TypeError hoặc tạo field sai chỗ trong schema.

// SAI — nếu json.featureFlags là undefined, lỗi TypeError
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.featureFlags.newDashboard = true;      // TypeError nếu featureFlags undefined
  await route.fulfill({ response, json });
});

// ĐÚNG — guard trước khi access nested
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  json.featureFlags = {
    ...(json.featureFlags ?? {}),             // safe spread nếu undefined
    newDashboard: true,
  };

  await route.fulfill({ response, json });
});

Pitfall 3 — Backend Down Không Có Fallback

Khi backend không accessible, route.fetch() throw error → test crash với message không rõ ràng. Cần bắt lỗi và có fallback hành vi rõ ràng.

// RISKY — không có xử lý khi fetch fail
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();        // throw nếu backend down
  const json = await response.json();
  json.role = 'admin';
  await route.fulfill({ response, json });
});

// TỐT HƠN — fallback về full mock khi backend không accessible
await page.route('**/api/user', async (route) => {
  try {
    const response = await route.fetch();
    const json = await response.json();
    json.role = 'admin';
    await route.fulfill({ response, json });
  } catch {
    // Fallback mock khi backend không có
    await route.fulfill({
      status: 200,
      json: { id: 1, name: 'Fallback User', role: 'admin' },
    });
  }
});

Pitfall 4 — Mutate Nested Object Gây Side-Effect

JavaScript object là reference — mutate trực tiếp nested object có thể ảnh hưởng không mong muốn nếu object được tái sử dụng trong cùng handler.

// RISKY — mutate trực tiếp nested object
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  // Mutate trực tiếp — nếu json.settings được dùng lại ở đây sau đó
  // có thể thấy giá trị đã bị thay
  json.settings.theme = 'dark';
  json.settings.language = 'vi';

  await route.fulfill({ response, json });
});

// AN TOÀN HƠN — tạo object mới cho nested
await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();

  json.settings = {
    ...json.settings,                        // spread tạo shallow copy
    theme: 'dark',
    language: 'vi',
  };

  await route.fulfill({ response, json });
});

Với nested object nhiều tầng, shallow spread chỉ an toàn tại tầng trực tiếp. Nếu cần patch sâu hơn, tạo copy tại tầng cần patch.

15

Tổng Kết

  • Pattern modify real: route.fetch() → parse JSON → patch field → route.fulfill({ response, json }).
  • Khác full mock: modify real giữ phần lớn data thật, chỉ override field cần thiết; full mock soạn toàn bộ data hardcode.
  • fulfill({ response, json }): response làm baseline (status + headers), json override body.
  • Nếu override headers, phải spread ...response.headers() trước rồi thêm field — không spread → mất toàn bộ headers thật.
  • Use case: test subscription tier/feature flag/A/B variant mà không seed DB; inject empty state, error field, missing field.
  • Patch array dùng .map() với spread object — không mutate trực tiếp item trong array.
  • Conditional modify: guard json.field && json.field.length > 0 trước khi patch nested.
  • Limitation: backend phải accessible; latency backend ảnh hưởng tốc độ test; schema thay đổi cần review patch logic; POST/PUT/DELETE có side-effect thật — cân nhắc dùng full mock.
  • 4 pitfall: quên response trong fulfill (mất status/headers), patch field không tồn tại (TypeError), backend down không có fallback, mutate nested reference (side-effect).
16

Quiz 5 Câu

Câu 1

Handler sau có gì sai?

await page.route('**/api/user', async (route) => {
  const response = await route.fetch();
  const json = await response.json();
  json.subscription = 'premium';
  await route.fulfill({ json });
});
  1. Thiếu await trước route.fulfill.
  2. Thiếu response trong fulfill({ json }) — status và headers gốc từ backend bị bỏ mất. Playwright dùng status 200 mặc định và headers tối thiểu. Nếu app phụ thuộc set-cookie hay custom header, behavior sai. Sửa: await route.fulfill({ response, json }).
  3. Không thể patch subscription trực tiếp lên object.
  4. Cần gọi response.dispose() trước khi fulfill.
Đáp án

B. fulfill({ json }) không có response → mất toàn bộ status và headers từ backend thật. Playwright fulfill với status 200 mặc định và chỉ set content-type: application/json. Với endpoint trả set-cookie để maintain session, hay custom header x-rate-limit, behavior test sẽ khác production. Pitfall 1 của bài.

Câu 2

Test cần bật feature flag newCheckout: true mà giữ nguyên các flag khác. Cú pháp nào đúng?

  1. json.featureFlags.newCheckout = true; — mutate trực tiếp.
  2. json.featureFlags = { newCheckout: true }; — thay thế toàn bộ flags.
  3. json.featureFlags = { ...json.featureFlags, newCheckout: true }; — spread flags hiện có, override field cần patch. Các flag còn lại giữ nguyên. An toàn với undefined vì nếu featureFlags undefined sẽ tạo object mới chỉ có newCheckout.
  4. json = { ...json, featureFlags: { newCheckout: true } };
Đáp án

C. Spread ...json.featureFlags giữ lại tất cả flag hiện có, rồi override newCheckout: true. A cũng hoạt động về chức năng nhưng mutate trực tiếp — rủi ro side-effect khi handler phức tạp. B sai — xóa sạch tất cả flag, chỉ còn newCheckout. D sai — object const không thể reassign với =, phải sửa field trực tiếp; đồng thời object featureFlags vẫn chỉ có newCheckout.

Câu 3

Để test empty state của trang orders mà không cần xóa data trong DB, cách nào ngắn nhất và đúng nhất?

  1. Fetch, parse, gán json = [], fulfill.
  2. const response = await route.fetch(); await route.fulfill({ response, json: [] }); — không cần parse body cũ, fulfill thẳng với json: []. Status và headers giữ nguyên từ backend.
  3. Dùng route.abort() để backend trả lỗi.
  4. Dùng route.continue() với body override.
Đáp án

B. Khi inject empty state, không cần inspect body gốc — bỏ qua bước parse, truyền thẳng json: [] để override body. response làm baseline giữ status 200 và headers. A sai về syntax — json là const từ response.json(), không thể reassign. C sai — abort trả network error, app render error state chứ không phải empty state. D sai — continue không có option override body.

Câu 4

Backend trả { user: { id: 1, settings: { theme: 'light' } } }. Handler patch như sau:

json.user.settings.theme = 'dark';
json.user.settings.notifications = true;
await route.fulfill({ response, json });

Rủi ro nào có thể xảy ra?

  1. TypeError vì không thể gán vào nested object.
  2. Mutate trực tiếp json.user.settings — nếu object này được tham chiếu ở chỗ khác trong handler (vd log, assert), sẽ thấy giá trị đã bị thay. Với handler đơn giản không có reuse, hoạt động bình thường nhưng không theo best practice. An toàn hơn: json.user.settings = { ...json.user.settings, theme: 'dark', notifications: true }.
  3. Playwright không cho phép patch nested object.
  4. Fulfill sẽ serialize sai vì nested quá sâu.
Đáp án

B. Pitfall 4 của bài. JavaScript là pass-by-reference cho object — mutate nested field thay đổi object gốc. Trong handler đơn giản với một luồng thực thi, thường không gây lỗi visible, nhưng là anti-pattern: nếu sau này handler phức tạp hơn (vd có log json.user.settings trước và sau patch), giá trị "trước" đã bị thay. Dùng spread { ...json.user.settings, theme: 'dark' } tạo object mới rõ ràng hơn về ý định.

Câu 5

Test cần modify response của POST /api/payment (tạo payment thật, không idempotent). Cách nào phù hợp nhất?

  1. Dùng route.fetch() + fulfill({ response, json }) — backend tạo payment thật, patch field status trong response.
  2. Dùng full mock route.fulfill({ json: mockPaymentResponse }) — không gọi backend, không tạo payment thật, không có side-effect. An toàn nhất cho endpoint không idempotent. Test chạy nhiều lần không để lại data rác trong DB.
  3. Dùng route.continue() để backend xử lý.
  4. Cả A và B đều phù hợp như nhau.
Đáp án

B. Limitation và pitfall 4 của bài — route.fetch() với POST không idempotent gọi backend thật, tạo payment thật trong DB test mỗi lần test chạy. Sau 100 lần chạy = 100 payment rác. Full mock route.fulfill({ json: mockResponse }) hoàn toàn tránh side-effect này, tối ưu hơn cho endpoint có side-effect. A kỹ thuật đúng nhưng để lại data rác. C không cho phép modify response. D sai — A và B không phù hợp như nhau về mặt side-effect.

17

Bài Tiếp Theo

Bài 120: Slow API Simulation — Simulate độ trễ mạng và backend chậm trong test để verify loading state, skeleton UI, timeout handling và user experience khi API không phản hồi nhanh.