Playwright λμ νκΈ°
μ¬λ΄ νλ‘μ νΈμ Playwrightλ₯Ό λμ νλ€. λΉλ‘κ·ΈμΈ μ¬μ©μκ° ν΄λν° λ²νΈλ₯Ό λ³κ²½νλ €λ©΄ λ¨Όμ μ΄λ©μΌλ‘ λ³ΈμΈ μΈμ¦μ ν΄μΌ νλ νλ‘μ°κ° μλ€. μ¬κΈ°μ μ΄λ©μΌ λ°μ‘ νμ μ ν(ν루 μ΅λ 10ν) μꡬμ¬νμ΄ μΆκ°λμλ€. μ¦, μ΄λ©μΌ μΈμ¦ μμ² API νΈμΆ μ 10νλ₯Ό μ΄κ³Όνλ©΄, μ¬μ©μμκ² μ ν λͺ¨λ¬μ λμ μλ €μΌ νλ€.
μ΄λ©μΌ μΈμ¦ μμ² APIκ° νΈμΆλλ κ²½μ°λ λ€μκ³Ό κ°λ€.
-
μ΄λ©μΌ μΈμ¦ νμ΄μ§μμ μ΄λ©μΌ μ λ ₯ ν μΈμ¦ λ²νΌ ν΄λ¦
-
μΈμ¦ μ΄λ©μΌ λ°μ‘ ν μ¬μ μ‘ λ²νΌ ν΄λ¦
-
μΈμ¦ μ΄λ©μΌ λ°μ‘ ν μΈμ¦ λ²νΈ μ ν¨μκ° λ§λ£ β λ§λ£ λͺ¨λ¬μμ μ¬μ μ‘ λ²νΌ ν΄λ¦
-
μ΄λ©μΌ μΈμ¦ μλ£ ν ν΄λν° λ²νΈ λ³κ²½ νλ‘μ° μ§μ μ, ν ν°μ΄ μλ κ²½μ° μ¬μΈμ¦ λͺ¨λ¬μμ μ¬μ μ‘ λ²νΌ ν΄λ¦
ν μ€νΈ μ½λ μμ±μ νμμ±
μ λͺ¨λ μΌμ΄μ€μμ μ¬μ©μκ° μ€μ λ‘ 10ν μ ν λͺ¨λ¬μ λ³΄κ² λλμ§ νμΈνλ €λ©΄, λ§€λ² APIλ₯Ό 10ν νΈμΆνκ³ , λ€λ₯Έ μΌμ΄μ€λ₯Ό μν΄ λ°±μλ κ°λ°μμκ² μ ν ν΄μ λ₯Ό μμ²ν λ€ λ€μ 10ν νΈμΆνλβ¦ μ΄λ° λ°©μμ 리μμ€ λλΉκ° ν¬κ³ λΉν¨μ¨μ μ΄λ€.
κ·Έλμ Playwright ν
μ€νΈλ₯Ό μμ±ν λ, API μμ²μ Mockingνμ¬ λ°λ‘ 10ν μ΄κ³Ό μλ΅μ λ°ννλλ‘ μ²λ¦¬νκ³ , UI λμλ§ κ²μ¦νλλ‘ μ€κ³νλ€.
λν, @faker-js/faker λ₯Ό μ¬μ©νμ¬ μ€μ μ κ°κΉμ΄ ν
μ€νΈ λ°μ΄ν°λ₯Ό νμ©νλ€.
λν ν μ€νΈ μ½λ
μλλ λνμ μΈ λ κ°μ§ μΌμ΄μ€μ μμμ΄λ€.
νμ¬ λ΄λΆ APIμ λ°μ΄ν°λ₯Ό 보νΈνκΈ° μν΄ URLκ³Ό μλ΅ λ±μ μΌλ°νλ μμ κ°μΌλ‘ λ체νλ€.
// app/e2e/μ΄λ©μΌ_μΈμ¦_μ΄κ³Ό.spec.ts
import { test, expect } from "@playwright/test";
import { faker } from "@faker-js/faker";
test("μΈμ¦νκΈ° λ²νΌ ν΄λ¦ μ μμ² 10νλ₯Ό μ΄κ³Όνλ©΄ λͺ¨λ¬ νμ", async ({
page,
}) => {
// μ΄λ©μΌ μ‘΄μ¬ νμΈ API mocking
await page.route(/\/api\/check/, (route) =>
route.fulfill({
status: 200,
body: JSON.stringify({ isUser: true }),
})
);
// μΈμ¦ μ½λ λ°μ‘ API mocking (10ν μ΄κ³Ό μλ΅)
// HTTP 429: Too Many Requests (ν루 μ΅λ μμ² νμ μ΄κ³Ό)
await page.route(/\/api\/send/, (route) =>
route.fulfill({
status: 429,
body: JSON.stringify({ code: "LIMIT_EXCEEDED" }),
})
);
await page.goto("/email-verification");
await page.locator('input[name="email"]').fill(faker.internet.email());
await page.getByRole("button", { name: "μΈμ¦νκΈ°" }).click();
// μ ν λͺ¨λ¬ νμ νμΈ
await expect(
page.getByText("ν루 μ΅λ 10ν μμ²μ΄ κ°λ₯ν©λλ€")
).toBeVisible();
});
test("μ¬μ μ‘ λ²νΌ ν΄λ¦ μ μμ² 10νλ₯Ό μ΄κ³Όνλ©΄ λͺ¨λ¬ νμ", async ({ page }) => {
// μ΄λ©μΌ μ‘΄μ¬ νμΈ API mocking
await page.route(/\/api\/check/, (route) =>
route.fulfill({
status: 200,
body: JSON.stringify({ isUser: true }),
})
);
// μΈμ¦ μ½λ λ°μ‘ API mocking (첫 μμ²μ μ±κ³΅ν΄μ μΈμ¦ μ½λ μ
λ ₯ UI κ° λ³΄μ¬μΌ νλ€)
await page.route(/\/api\/send/, (route) =>
route.fulfill({
status: 200,
body: JSON.stringify({ msg: "μΈμ¦ μ½λ λ°μ‘ μ±κ³΅" }),
})
);
await page.goto("/email-verification");
await page.locator('input[name="email"]').fill(faker.internet.email());
await page.getByRole("button", { name: "μΈμ¦νκΈ°" }).click();
// μ¬μ μ‘ μ 10ν μ΄κ³Ό μλ΅ mocking
// κΈ°μ‘΄ Mock μ κ±° ν μ Mock μ μ© β λμΌ URLμ λ€λ₯Έ μλλ¦¬μ€ ν
μ€νΈ κ°λ₯
page.unroute(/\/api\/send/);
page.route(/\/api\/send/, (route) =>
route.fulfill({
status: 429,
body: JSON.stringify({ code: "LIMIT_EXCEEDED" }),
})
);
await page.getByText("μ¬μ μ‘").click();
// μ ν λͺ¨λ¬ νμ νμΈ
await expect(
page.getByText("ν루 μ΅λ 10ν μμ²μ΄ κ°λ₯ν©λλ€")
).toBeVisible();
// μΈμ¦ μ½λ μ
λ ₯ νμ΄λ¨Έ μ΄κΈ°ν νμΈ
expect(await page.locator('[data-test-id="timer"]').textContent()).toBe(
"00:00"
);
});
Playwrightμμ API Mocking νκΈ°
μ μμμ²λΌ Playwrightμμλ page.route()λ₯Ό μ΄μ©ν΄ νΉμ μμ²μ κ°λ‘μ±κ³ , μνλ μλ΅(Mock)μ λ°νν μ μλ€.
μ΄λ₯Ό ν΅ν΄ μ€μ μλ² νΈμΆ μμ΄λ λ€μν μλ리μ€λ₯Ό ν
μ€νΈν μ μλ€.
// route.fulfill(): μ€μ μλ² νΈμΆ μμ΄ μ§μ ν Mock μλ΅μ μ¦μ λ°ν
await page.route(/\/api\/send/, async (route) => {
await route.fulfill({
status: 429,
body: JSON.stringify({ code: "LIMIT_EXCEEDED" }),
});
});
μ μ½λλ /api/code
νΈμΆμ μλ²κ° 10ν μ΄κ³Ό μλ΅μ λ°ννλ κ²μ²λΌ λμνκ² νλ€.
Playwrightλ μ κ· ννμμ μ§μνλ―λ‘, νΉμ ν¨ν΄μ λ§λ URLμ ν λ²μ κ°λ‘μ± μ μλ€.
route.fulfill()μ ν΄λΉ μμ²μ μ¦μ Mock μλ΅μΌλ‘ μ²λ¦¬νλ©°, μ€μ μλ²λ‘ μμ²μ 보λ΄μ§ μλλ€. λ°λ©΄ route.fetch()λ₯Ό μ¬μ©νλ©΄ μ€μ μλ²λ‘ μμ²μ 보λ΄κ³ , μλ΅μ λ³νν΄μ μ λ¬ν μ μλ€.
μ΄λ² ν
μ€νΈμμλ λ€μ μ΄μ λ‘ route.fulfill()
μ μ¬μ©νλ€:
- μ€μ μλ² νΈμΆ λΆνμ
- UI κ²μ¦κ³Ό 10ν μ΄κ³Ό μλλ¦¬μ€ ν μ€νΈκ° λͺ©μ μ΄λ―λ‘, μλ² μμ² μμ΄ Mock μλ΅λ§μΌλ‘ μΆ©λΆνλ€.
- ν μ€νΈ 격리
- λ°±μλ μνλ λ°μ΄ν°μ 무κ΄νκ² νμ λμΌν 쑰건μμ ν μ€νΈ κ°λ₯
- ν¨μ¨μ±
- λ§€λ² μ€μ μλ² νΈμΆ μμ΄λ λ€μν μλ리μ€(μ±κ³΅, μ ν μ΄κ³Ό)λ₯Ό μ¦μ ν μ€νΈν μ μλ€.
μ¦, μ΄λ² e2e ν
μ€νΈλ UI λμ κ²μ¦μ μ§μ€νκ³ , μλ²μμ μνΈμμ©μ μμ ν MockμΌλ‘ λ체νκΈ° λλ¬Έμ route.fulfill()
μ΄ κ°μ₯ μ ν©νλ€.
μ°Έκ³ : route.fetch()
// route.fetch() μ¬μ© μμ
// μ€μ μλ² μμ²μ 보λ΄κ³ μλ΅μ μΌλΆ μμ ν΄μ λ°ν κ°λ₯
await page.route("**/api/data", async (route) => {
// 1. μλ μμ²μ μλ²μ κ·Έλλ‘ λ³΄λ΄κ³ μλ΅ λ°κΈ°
const response = await route.fetch();
const data = await response.json();
// 2. μλ΅ λ°μ΄ν° μΌλΆ μμ
const modifiedData = { ...data, modified: true };
// 3. μμ ν λ°μ΄ν°λ₯Ό ν΄λΌμ΄μΈνΈμ λ°ν
await route.fulfill({
body: JSON.stringify(modifiedData),
});
});
κΈ°μ‘΄ Mockμ μ κ±°νκ³ μ Mock μ μ©: page.unroute()
λμΌν URLμ λν΄ λ€λ₯Έ μλ리μ€λ₯Ό ν μ€νΈν λλ, κΈ°μ‘΄ Mockμ μ κ±°νκ³ μ Mockμ μ μ©ν μ μλ€.
// κΈ°μ‘΄ Mock μ κ±°
page.unroute(/\/api\/send/);
// μλ‘μ΄ Mock μ μ©
page.route(/\/api\/send/, async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify({ msg: "μΈμ¦ μ½λ λ°μ‘ μ±κ³΅" }),
});
});
page.unroute()λ₯Ό μ¬μ©νλ©΄ μ΄μ Mockμ΄ λ μ΄μ μ μ©λμ§ μμΌλ―λ‘, μ¬μ μ‘ λ²νΌ ν΄λ¦ μ λ€λ₯Έ μλ리μ€λ₯Ό μ½κ² ν μ€νΈν μ μλ€.
μ΄λ κ² Mockκ³Ό unrouteλ₯Ό μ‘°ν©νλ©΄, λ¨μΌ ν μ€νΈ λ΄μμ λ€μν API μλ΅ μΌμ΄μ€λ₯Ό κ²μ¦ν μ μλ€. (μ΄κΈ°μλ μμ²μ΄ μ±κ³΅νκ³ , μ΄νμλ μ€ν¨νλ μΌμ΄μ€ κ²μ¦ λ±)
λ§μΉλ©°
Playwrightλ₯Ό νμ©ν Mock κΈ°λ° e2e ν μ€νΈ λλΆμ, λ°±μλμ μμ‘΄νμ§ μκ³ νλ‘ νΈμμ λ 립μ μΌλ‘ UI λμμ κ²μ¦ν μ μμλ€. μ΄λ‘ μΈν΄ κ°λ° μμ°μ±μ΄ ν¬κ² ν₯μλμκ³ , νμλ€μκ²λ e2e ν μ€νΈ λμ μ μκ°νλ μ’μ κΈ°νκ° λμλ€.
νΉν QA μ§ν κ³Όμ μμ κΈ°λ₯μ΄λ UI κ΄λ ¨ μ΄μκ° λ°μνμ§ μμ, κΈ°λ₯μ μμ μ±μ λμ΄λ ν¨κ³Όλ₯Ό μ§μ νμΈν μ μμλ€.