E2E ํ ์คํธ๋ ๋ฌด์์ธ๊ฐ
์น ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค์๋ค๋ฉด,
์ค์ ๋ก ์ ๋์ํ๋์ง ํ์ธํด์ผ ํ๋ค.
E2E(End-to-End) ํ
์คํธ๋
์ค์ ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ๊ฒ์ฒ๋ผ
์ฒ์๋ถํฐ ๋๊น์ง ์ ์ฒด ์๋๋ฆฌ์ค๋ฅผ ํ
์คํธํ๋ ๋ฐฉ๋ฒ์ด๋ค.
๋ง์น ์๋์ฐจ ๊ณต์ฅ์์
์์ง, ๋ฐํด, ๋ฌธ์ ๋ฐ๋ก ํ
์คํธํ๊ณ (๋จ์ ํ
์คํธ)
์กฐ๋ฆฝ ํ ์ค์ ๋ก ๋๋ก๋ฅผ ์ฃผํํด๋ณด๋ ๊ฒ (E2E ํ
์คํธ)
์ฒ๋ผ ๋ง์ด๋ค.
์ E2E ํ ์คํธ๋ฅผ ํด์ผ ํ ๊น?
์ด์ 1: ์ค์ ์ฌ์ฉ์ ๊ฒฝํ ๊ฒ์ฆ
๊ฐ ๋ถ๋ถ์ด ์ ๋์ํด๋ ์ ์ฒด๊ฐ ์ ๋ ์ ์๋ค.
์ด์ 2: ํ๊ท ๋ฒ๊ทธ ๋ฐฉ์ง
์ ๊ธฐ๋ฅ ์ถ๊ฐ ์ ๊ธฐ์กด ๊ธฐ๋ฅ์ด ๋ง๊ฐ์ง๋ ๊ฒ์ ๋ฐฉ์ง.
์ด์ 3: ์๋ํ๋ก ์๊ฐ ์ ์ฝ
์๋ ํ
์คํธ๋ ์๊ฐ์ด ๋ง์ด ๊ฑธ๋ฆฌ๊ณ ์ค์ํ ์ ์๋ค.
์ด์ 4: ์์ ๊ฐ ์๋ ๋ฐฐํฌ
ํ
์คํธ๊ฐ ํต๊ณผํ๋ฉด ์์ฌํ๊ณ ๋ฐฐํฌํ ์ ์๋ค.
๊ธฐ๋ณธ ๊ฐ๋ ์์ฝ
๐ท๏ธ ํ ์คํธ ํผ๋ผ๋ฏธ๋
/\
/E2E\ ์ ์, ๋๋ฆผ, ๋น์ฉ ๋์
/------\
/ํตํฉ ํ
์คํธ\ ์ค๊ฐ
/----------\
/ ๋จ์ ํ
์คํธ \ ๋ง์, ๋น ๋ฆ, ๋น์ฉ ๋ฎ์
/--------------\
1. ๋จ์ ํ ์คํธ (Unit Test)
๊ฐ๋
: ํจ์๋ ๋ฉ์๋ ๋จ์๋ก ํ
์คํธ
์์:
๋ํ๊ธฐ ํจ์๊ฐ 2 + 3 = 5๋ฅผ ๋ฐํํ๋์ง ํ์ธ
ํน์ง:
- ๋น ๋ฆ (๋ฐ๋ฆฌ์ด ๋จ์)
- ๋ง์ด ์์ฑ (์๋ฐฑ~์์ฒ ๊ฐ)
- ๋ฒ๊ทธ ์์น ๋ช ํ
2. ํตํฉ ํ ์คํธ (Integration Test)
๊ฐ๋
: ์ฌ๋ฌ ๋ชจ๋์ด ํจ๊ป ๋์ํ๋์ง ํ
์คํธ
์์:
ํ์๊ฐ์
API๊ฐ DB์ ์ ์ฅ๋๋์ง ํ์ธ
ํน์ง:
- ์ค๊ฐ ์๋ (์ด ๋จ์)
- ์ค๊ฐ ๊ฐ์ (์์ญ~์๋ฐฑ ๊ฐ)
- ๋ชจ๋ ๊ฐ ์ฐ๋ ํ์ธ
3. E2E ํ ์คํธ (End-to-End Test)
๊ฐ๋
: ์ค์ ์ฌ์ฉ์์ฒ๋ผ ์ ์ฒด ์๋๋ฆฌ์ค ํ
์คํธ
์์:
๋ก๊ทธ์ธ โ ์ํ ๊ฒ์ โ ์ฅ๋ฐ๊ตฌ๋ โ ๊ฒฐ์ ์ ์ฒด ํ๋ฆ
ํน์ง:
- ๋๋ฆผ (๋ถ ๋จ์)
- ์ ๊ฒ ์์ฑ (์์ญ ๊ฐ)
- ์ค์ ์ฌ์ฉ์ ๊ฒฝํ ๊ฒ์ฆ
๐ท๏ธ E2E ํ ์คํธ ๋๊ตฌ ๋น๊ต
| ๋๊ตฌ | ์ธ์ด | ๋ธ๋ผ์ฐ์ | ์๋ | ํน์ง |
|---|---|---|---|---|
| Playwright | JS/TS | Chrome, Firefox, Safari | ๋น ๋ฆ | ์ต์ ๊ธฐ์ , ์๋ ๋๊ธฐ |
| Cypress | JS/TS | Chrome, Edge | ๋น ๋ฆ | ๊ฐ๋ฐ์ ์นํ์ , ๋๋ฒ๊น ์ข์ |
| Selenium | ๋ค์ | ๋ชจ๋ ๋ธ๋ผ์ฐ์ | ๋๋ฆผ | ์ค๋๋จ, ์์ ์ |
| Puppeteer | JS | Chrome๋ง | ๋น ๋ฆ | ํค๋๋ฆฌ์ค ํนํ |
๐ท๏ธ Playwright ํต์ฌ ๊ฐ๋
1. Browser Context
๊ฐ๋
: ๋
๋ฆฝ๋ ๋ธ๋ผ์ฐ์ ์ธ์
(์ฟ ํค, ๋ก์ปฌ ์คํ ๋ฆฌ์ง ๊ฒฉ๋ฆฌ)
์ํฌ๋ฆฟ ๋ชจ๋ ๋น์ :
๊ฐ Context๋ ๋
๋ฆฝ๋ ์ํฌ๋ฆฟ ์ฐฝ์ฒ๋ผ ๋์
2. Page
๊ฐ๋
: ๋ธ๋ผ์ฐ์ ํญ
3. Locator
๊ฐ๋
: ์์๋ฅผ ์ฐพ๋ ๋ฐฉ๋ฒ
// CSS Selector
page.locator('.submit-button')
// Text๋ก ์ฐพ๊ธฐ
page.locator('text=๋ก๊ทธ์ธ')
// Role๋ก ์ฐพ๊ธฐ
page.getByRole('button', { name: '์ ์ถ' })
4. Auto-waiting
๊ฐ๋
: ์๋์ผ๋ก ์์๊ฐ ๋ํ๋ ๋๊น์ง ๋๊ธฐ
// โ Selenium (๋ช
์์ ๋๊ธฐ ํ์)
await driver.wait(until.elementLocated(By.id('button')), 5000);
// โ
Playwright (์๋ ๋๊ธฐ)
await page.click('#button'); // ์๋์ผ๋ก ๋๊ธฐ
์ค์ ์์
๐ท๏ธ Playwright ์ค์น ๋ฐ ์ค์
1. ์ค์น
npm init playwright@latest
# ์ค์น ์ต์
โ Do you want to use TypeScript or JavaScript? ยท TypeScript
โ Where to put your end-to-end tests? ยท tests
โ Add a GitHub Actions workflow? ยท Yes
2. ํ๋ก์ ํธ ๊ตฌ์กฐ
project/
โโโ tests/
โ โโโ login.spec.ts
โ โโโ checkout.spec.ts
โ โโโ search.spec.ts
โโโ playwright.config.ts
โโโ package.json
3. ์ค์ ํ์ผ (playwright.config.ts)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// ํ์์์
timeout: 30 * 1000,
// ์ฌ์๋
retries: process.env.CI ? 2 : 0,
// ๋ณ๋ ฌ ์คํ
workers: process.env.CI ? 1 : undefined,
// ๋ฆฌํฌํฐ
reporter: 'html',
use: {
// ๊ธฐ๋ณธ URL
baseURL: 'http://localhost:3000',
// ์คํฌ๋ฆฐ์ท
screenshot: 'only-on-failure',
// ๋น๋์ค
video: 'retain-on-failure',
// ํธ๋ ์ด์ค
trace: 'on-first-retry',
},
// ํ๋ก์ ํธ (๋ธ๋ผ์ฐ์ ๋ณ)
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
// ๊ฐ๋ฐ ์๋ฒ ์๋ ์์
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
๐ท๏ธ ๋ก๊ทธ์ธ ํ ์คํธ
import { test, expect } from '@playwright/test';
test.describe('๋ก๊ทธ์ธ ๊ธฐ๋ฅ', () => {
test.beforeEach(async ({ page }) => {
// ๊ฐ ํ
์คํธ ์ ์ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
await page.goto('/login');
});
test('์ฑ๊ณต์ ์ธ ๋ก๊ทธ์ธ', async ({ page }) => {
// 1. ์
๋ ฅ ํ๋ ์ฐพ์์ ์
๋ ฅ
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
// 2. ๋ก๊ทธ์ธ ๋ฒํผ ํด๋ฆญ
await page.click('button:has-text("๋ก๊ทธ์ธ")');
// 3. ํ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ์ธ
await expect(page).toHaveURL('/home');
// 4. ํ์ ๋ฉ์์ง ํ์ธ
await expect(page.locator('text=ํ์ํฉ๋๋ค')).toBeVisible();
});
test('์๋ชป๋ ๋น๋ฐ๋ฒํธ', async ({ page }) => {
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'wrongpassword');
await page.click('button:has-text("๋ก๊ทธ์ธ")');
// ์๋ฌ ๋ฉ์์ง ํ์ธ
await expect(page.locator('.error-message'))
.toHaveText('์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค');
});
test('๋น ํ๋ ๊ฒ์ฆ', async ({ page }) => {
await page.click('button:has-text("๋ก๊ทธ์ธ")');
// HTML5 validation ํ์ธ
const usernameInput = page.locator('input[name="username"]');
await expect(usernameInput).toHaveAttribute('required', '');
});
});
๐ท๏ธ ์ผํ๋ชฐ E2E ํ ์คํธ
import { test, expect } from '@playwright/test';
test('์ํ ๊ตฌ๋งค ์ ์ฒด ํ๋ฆ', async ({ page }) => {
// 1. ํํ์ด์ง ๋ฐฉ๋ฌธ
await page.goto('/');
await expect(page).toHaveTitle(/์ผํ๋ชฐ/);
// 2. ์ํ ๊ฒ์
await page.fill('input[name="search"]', '๋
ธํธ๋ถ');
await page.press('input[name="search"]', 'Enter');
// ๊ฒ์ ๊ฒฐ๊ณผ ํ์ธ
await expect(page.locator('.product-list .product')).toHaveCount(10, {
timeout: 5000
});
// 3. ์ฒซ ๋ฒ์งธ ์ํ ํด๋ฆญ
await page.click('.product-list .product:first-child');
// ์ํ ์์ธ ํ์ด์ง ํ์ธ
await expect(page.locator('h1.product-title')).toBeVisible();
// 4. ์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ
await page.click('button:has-text("์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ")');
// ์ฑ๊ณต ํ ์คํธ ๋ฉ์์ง ํ์ธ
await expect(page.locator('.toast.success'))
.toHaveText('์ฅ๋ฐ๊ตฌ๋์ ์ถ๊ฐ๋์์ต๋๋ค');
// 5. ์ฅ๋ฐ๊ตฌ๋๋ก ์ด๋
await page.click('a[href="/cart"]');
await expect(page).toHaveURL('/cart');
// ์ฅ๋ฐ๊ตฌ๋ ์์ดํ
ํ์ธ
await expect(page.locator('.cart-item')).toHaveCount(1);
// 6. ์๋ ๋ณ๊ฒฝ
await page.click('.quantity-increase');
await expect(page.locator('.quantity-display')).toHaveText('2');
// ์ด ๊ฐ๊ฒฉ ์
๋ฐ์ดํธ ํ์ธ
const totalPrice = await page.locator('.total-price').textContent();
expect(totalPrice).toContain('2,000,000'); // ์์
// 7. ๊ฒฐ์ ํ์ด์ง๋ก ์ด๋
await page.click('button:has-text("์ฃผ๋ฌธํ๊ธฐ")');
await expect(page).toHaveURL('/checkout');
// 8. ๋ฐฐ์ก ์ ๋ณด ์
๋ ฅ
await page.fill('input[name="name"]', 'ํ๊ธธ๋');
await page.fill('input[name="phone"]', '010-1234-5678');
await page.fill('input[name="address"]', '์์ธ์ ๊ฐ๋จ๊ตฌ');
// 9. ๊ฒฐ์ ์๋จ ์ ํ
await page.check('input[value="card"]');
// 10. ์ฃผ๋ฌธ ์๋ฃ
await page.click('button:has-text("๊ฒฐ์ ํ๊ธฐ")');
// ์ฃผ๋ฌธ ์๋ฃ ํ์ด์ง ํ์ธ
await expect(page).toHaveURL(/\/order\/\d+/);
await expect(page.locator('.order-complete'))
.toHaveText('์ฃผ๋ฌธ์ด ์๋ฃ๋์์ต๋๋ค');
});
๐ท๏ธ API ์๋ต ๋๊ธฐ ๋ฐ ๋ชจํน
API ์๋ต ๋๊ธฐ
test('๊ฒ์ ํ ๊ฒฐ๊ณผ ํ์ธ', async ({ page }) => {
await page.goto('/');
// API ์๋ต ๋๊ธฐ
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/search') && response.status() === 200
);
await page.fill('input[name="search"]', '๋
ธํธ๋ถ');
await page.press('input[name="search"]', 'Enter');
const response = await responsePromise;
const data = await response.json();
// API ์๋ต ๊ฒ์ฆ
expect(data.results).toHaveLength(10);
// UI ์
๋ฐ์ดํธ ํ์ธ
await expect(page.locator('.product')).toHaveCount(10);
});
API ๋ชจํน
test('API ๋ชจํน์ผ๋ก ๋น ๋ฅธ ํ
์คํธ', async ({ page }) => {
// API ์๋ต ๋ชจํน
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
products: [
{ id: 1, name: 'ํ
์คํธ ์ํ 1', price: 10000 },
{ id: 2, name: 'ํ
์คํธ ์ํ 2', price: 20000 },
]
})
});
});
await page.goto('/products');
// ๋ชจํน๋ ๋ฐ์ดํฐ๋ก ๋ ๋๋ง ํ์ธ
await expect(page.locator('.product')).toHaveCount(2);
await expect(page.locator('.product:first-child .name'))
.toHaveText('ํ
์คํธ ์ํ 1');
});
๐ท๏ธ Page Object Model (POM)
๊ฐ๋
: ํ์ด์ง๋ณ๋ก ํด๋์ค๋ฅผ ๋ง๋ค์ด ์ฌ์ฌ์ฉ์ฑ ํฅ์
LoginPage ํด๋์ค
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
// Locators
private usernameInput = () => this.page.locator('input[name="username"]');
private passwordInput = () => this.page.locator('input[name="password"]');
private loginButton = () => this.page.locator('button:has-text("๋ก๊ทธ์ธ")');
private errorMessage = () => this.page.locator('.error-message');
// Actions
async goto() {
await this.page.goto('/login');
}
async login(username: string, password: string) {
await this.usernameInput().fill(username);
await this.passwordInput().fill(password);
await this.loginButton().click();
}
async getErrorMessage() {
return await this.errorMessage().textContent();
}
// Assertions
async expectErrorMessage(message: string) {
await expect(this.errorMessage()).toHaveText(message);
}
}
ํ ์คํธ์์ ์ฌ์ฉ
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('๋ก๊ทธ์ธ ์คํจ', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'wrongpassword');
await loginPage.expectErrorMessage('์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์ต๋๋ค');
});
๐ท๏ธ ์ธ์ฆ ์ํ ์ฌ์ฌ์ฉ
๊ฐ๋
: ๋งค๋ฒ ๋ก๊ทธ์ธํ์ง ์๊ณ ์ธ์ฆ ์ํ ์ ์ฅ
์ธ์ฆ ์ํ ์ ์ฅ
// auth.setup.ts
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="username"]', 'testuser');
await page.fill('input[name="password"]', 'password123');
await page.click('button:has-text("๋ก๊ทธ์ธ")');
// ๋ก๊ทธ์ธ ์๋ฃ ๋๊ธฐ
await page.waitForURL('/home');
// ์ธ์ฆ ์ํ ์ ์ฅ
await page.context().storageState({ path: authFile });
});
์ ์ฅ๋ ์ธ์ฆ ์ํ ์ฌ์ฉ
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
// ํ
์คํธ๋ ์ด๋ฏธ ๋ก๊ทธ์ธ๋ ์ํ๋ก ์์
test('ํ๋กํ ์์ ', async ({ page }) => {
await page.goto('/profile'); // ์ด๋ฏธ ๋ก๊ทธ์ธ๋จ
// ...
});
Best Practices
๐ท๏ธ 1. ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ด์ด์ผ ํจ
// โ Bad: ํ
์คํธ ๊ฐ ์์กด์ฑ
test('์ฌ์ฉ์ ์์ฑ', async ({ page }) => {
// user๋ฅผ ์ ์ญ ๋ณ์์ ์ ์ฅ
});
test('์ฌ์ฉ์ ์์ ', async ({ page }) => {
// ์ด์ ํ
์คํธ์ user ์ฌ์ฉ
});
// โ
Good: ๊ฐ ํ
์คํธ๊ฐ ๋
๋ฆฝ์
test('์ฌ์ฉ์ ์์ ', async ({ page }) => {
// ํ
์คํธ ๋ด์์ ์ฌ์ฉ์ ์์ฑ
const user = await createTestUser();
// ์์ ํ
์คํธ
// ์ ๋ฆฌ
await deleteTestUser(user.id);
});
๐ท๏ธ 2. ์์ ์ ์ธ Locator ์ฌ์ฉ
// โ Bad: ๊นจ์ง๊ธฐ ์ฌ์ด Locator
page.locator('div > div:nth-child(3) > button')
page.locator('button').nth(5)
// โ
Good: ์๋ฏธ ์๋ Locator
page.getByRole('button', { name: '์ ์ถ' })
page.getByTestId('submit-button')
page.getByLabel('์ฌ์ฉ์ ์ด๋ฆ')
๐ท๏ธ 3. ๋ช ์์ ์ธ ๋๊ธฐ ํผํ๊ธฐ
// โ Bad: ๊ณ ์ ๋ ๋๊ธฐ ์๊ฐ
await page.click('button');
await page.waitForTimeout(3000); // ํญ์ 3์ด ๋๊ธฐ
// โ
Good: ์๋ ๋๊ธฐ ๋๋ ์กฐ๊ฑด ๋๊ธฐ
await page.click('button');
await page.waitForLoadState('networkidle');
await page.waitForSelector('.result');
๐ท๏ธ 4. ํ ์คํธ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ
// โ
Good: ๊ณ ์ ํ ํ
์คํธ ๋ฐ์ดํฐ
test('์ฌ์ฉ์ ๋ฑ๋ก', async ({ page }) => {
const uniqueEmail = `test${Date.now()}@example.com`;
await page.goto('/register');
await page.fill('input[name="email"]', uniqueEmail);
// ...
});
๐ท๏ธ 5. ์คํฌ๋ฆฐ์ท๊ณผ ๋น๋์ค ํ์ฉ
test('์ค์ํ ๊ธฐ๋ฅ', async ({ page }) => {
await page.goto('/dashboard');
// ํน์ ์์ ์คํฌ๋ฆฐ์ท
await page.screenshot({ path: 'dashboard.png' });
// ํน์ ์์๋ง ์คํฌ๋ฆฐ์ท
await page.locator('.chart').screenshot({ path: 'chart.png' });
// ์ ์ฒด ํ์ด์ง ์คํฌ๋ฆฐ์ท
await page.screenshot({ path: 'full-page.png', fullPage: true });
});
์ค์ ์ฒดํฌ๋ฆฌ์คํธ
โ ํ ์คํธ ์์ฑ
- ํต์ฌ ์ฌ์ฉ์ ํ๋ฆ ์ปค๋ฒ
- ํ๋ณต ๊ฒฝ๋ก + ์๋ฌ ์ผ์ด์ค
- ๋ชจ๋ฐ์ผ ๋ฐ์ํ ํ ์คํธ
- ํฌ๋ก์ค ๋ธ๋ผ์ฐ์ ํ ์คํธ
โ ํ ์คํธ ์์ ์ฑ
- ์์ ์ ์ธ Locator ์ฌ์ฉ
- ์๋ ๋๊ธฐ ํ์ฉ
- ํ ์คํธ ๋ ๋ฆฝ์ฑ ๋ณด์ฅ
- ํ ์คํธ ๋ฐ์ดํฐ ๊ฒฉ๋ฆฌ
โ ์ฑ๋ฅ
- ๋ณ๋ ฌ ์คํ ์ค์
- ์ธ์ฆ ์ํ ์ฌ์ฌ์ฉ
- ๋ถํ์ํ ๋๊ธฐ ์ ๊ฑฐ
- API ๋ชจํน ํ์ฉ
โ ๋๋ฒ๊น
- ์คํจ ์ ์คํฌ๋ฆฐ์ท
- ์คํจ ์ ๋น๋์ค ๋ นํ
- Trace ํ์ผ ์์ฑ
- ๋ช ํํ ์๋ฌ ๋ฉ์์ง
โ CI/CD ํตํฉ
- GitHub Actions ์ค์
- ์๋ ์ฌ์๋ ์ค์
- ๋ณ๋ ฌ ์คํ ์ต์ ํ
- ํ ์คํธ ๊ฒฐ๊ณผ ๋ฆฌํฌํธ
์์ฝ
E2E ํ
์คํธ๋ ์ค์ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ฒ์ฆํ๋ ํ์ ๋๊ตฌ๋ค.
๐ ํต์ฌ ํฌ์ธํธ:
- ํ ์คํธ ํผ๋ผ๋ฏธ๋: ๋จ์ > ํตํฉ > E2E ์์ผ๋ก ๋ง์ด ์์ฑ
- Playwright: ์ต์ ๊ธฐ์ , ๋น ๋ฅด๊ณ ์์ ์
- ๋ ๋ฆฝ์ฑ: ๊ฐ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ผ๋ก ์คํ ๊ฐ๋ฅ
- ์์ ์ฑ: ์๋ฏธ ์๋ Locator, ์๋ ๋๊ธฐ
- ์ฌ์ฌ์ฉ: Page Object Model ํ์ฉ
- ํจ์จ์ฑ: ์ธ์ฆ ์ํ ์ฌ์ฌ์ฉ, API ๋ชจํน
๐ฏ ํ ์คํธ ์์ฑ ์์:
- ํต์ฌ ์ฌ์ฉ์ ํ๋ฆ ํ์
- ํ๋ณต ๊ฒฝ๋ก ์๋๋ฆฌ์ค ์์ฑ
- ์๋ฌ ์ผ์ด์ค ์ถ๊ฐ
- ์ฃ์ง ์ผ์ด์ค ์ปค๋ฒ
๐ Best Practices:
- ๋ช ํํ ํ ์คํธ๋ช : โ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธํ๊ณ ์ํ์ ๊ตฌ๋งคํ๋คโ
- AAA ํจํด: Arrange (์ค๋น) โ Act (์คํ) โ Assert (๊ฒ์ฆ)
- ๋จ์ผ ์ฑ ์: ํ๋์ ํ ์คํธ๋ ํ๋์ ์๋๋ฆฌ์ค๋ง
- ๋ ๋ฆฝ์ฑ: ํ ์คํธ ์์์ ์์กดํ์ง ์์
- ์ฌํ ๊ฐ๋ฅ: ์ธ์ ์คํํด๋ ๊ฐ์ ๊ฒฐ๊ณผ
๐ Playwright vs Cypress:
| ํญ๋ชฉ | Playwright | Cypress |
|---|---|---|
| ๋ธ๋ผ์ฐ์ | Chrome, Firefox, Safari | Chrome, Edge |
| ์๋ | ๋น ๋ฆ | ๋น ๋ฆ |
| API ๋ชจํน | ๋ด์ฅ | ๋ด์ฅ |
| ๋๋ฒ๊น | Trace Viewer | Time Travel |
| ๋ฌ๋์ปค๋ธ | ๋ฎ์ | ๋ฎ์ |
E2E ํ
์คํธ๋ ์ฒ์์๋ ์์ฑ ์๊ฐ์ด ๊ฑธ๋ฆฌ์ง๋ง,
ํ ๋ฒ ์์ฑํ๋ฉด ์๋ฐฑ ๋ฒ ์ฌ์ฌ์ฉํ ์ ์๋ค.
์๋ํ๋ ํ
์คํธ๋ ๋ฒ๊ทธ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ๊ณ ,
์์ ๊ฐ ์๊ฒ ๋ฐฐํฌํ ์ ์๊ฒ ํด์ค๋ค.