E2E(End to End) Test
์ค์ ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์์ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฒด๋ฅผ ๋์ํด์ ๊ธฐ๋ฅ์ ํ์ธํ๋ ํ
์คํธ
์ฆ, ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ์ํํธ์จ์ด๋ฅผ ์ฌ์ฉํ์๋ ํ๋ก์ฐ๋ฅผ ํ
์คํธํ๋ ๊ฒ
E2E Test ํน์ง
์ค์ ์ฌ์ฉ์ ํ๊ฒฝ์์ ํ
์คํธํ๋ฏ๋ก ์ฌ์ฉ์์ ์
์ฅ์์์ ๊ฒฝํ์ ์ง์ ์ ์ผ๋ก ํ
์คํธํ ์ ์๋ค.
์ ์ฒด ์์คํ
์ ๋์์ผ๋ก ํ
์คํธ๋ฅผ ์ํํ๊ธฐ ๋๋ฌธ์ ๊ฐ๊ฐ์ ๋ชจ๋์ด๋ ์ปดํฌ๋ํธ๊ฐ ํจ๊ป ์๋ํ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋ค์ ํ์
๊ฐ๋ฅ
์ธ๋ถ ํ๊ฒฝ์ด๋ ๋คํธ์ํฌ๋ฅผ ์ด์ฉํ๋ค๋ฉด ์กฐ๊ธ ๋ถ์์ ํ ์ ์๋ค.
E2E Test Framework
Cypress
Cypress
์ ์์ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด UI ์์๋ฅผ ๊ฐ์ ธ์ฌ ๋ ๋ด๋ถ ๊ตฌํ ์ฌํญ (์: class)์ ์์กดํ๊ณ ์๋ค.
testing-library๋ Cypress๋ ์ง์ํ๋ฏ๋ก ํจ๊ป ์ฌ์ฉํ๋ฉด UI ์์๋ฅผ ์ฌ์ฉ์ ๊ด์ ์์ ๊ฐ์ ธ์ฌ ์ ์๋ค
Copy npm i -D @testing-library/cypress
E2E ํ
์คํธ์์๋ ๋ง์ฐฌ๊ฐ์ง๋ก, ๋คํธ์ํฌ ๊ด๋ จ ์์กด์ฑ๋ค์ Mock ๋ฐ์ดํฐ๋ฅผ ํ์ฉํ์ฌ ํ
์คํธ๋ฅผ ์งํํ๋ค.
์ด๋ ์ฌ์ฉ์์ ์ค์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ํ๊ฒฝ์์ ํ
์คํธ๋ฅผ ์ํํ๋๋ผ๋ ์ธ๋ถ ์์กด์ฑ์ ์ํด ํ
์คํธ๊ฐ ์คํจํ๋ ๊ฒ์ ๋ฐฉ์งํ๊ธฐ ์ํจ์ด๋ค.
Copy import '@testing-library/cypress/add-commands'
describe('App', () => {
beforeEach(() => {
cy.intercept('GET', '/restaurants', {
fixture: 'restaurants.json',
})
cy.visit('http://localhost:3001/');
})
it('display text search in the header', () => {
cy.findByText(/search/).should('exist');
})
})
Headless Chrome
Headless ์ํ์์๋ ํฌ๋กฌ ๋ธ๋ผ์ฐ์ ๋ฅผ ์คํํ ์ ์๋ ๋ฐฉ๋ฒ, ํฌ๋กฌ์์ด ํฌ๋กฌ์ ์คํํ๋ ๊ฒ์ด๋ผ๊ณ ์ค๋ช
ํ๋๋ฐ ํฌ๋กค๋ง์ ํ๊ฑฐ๋ E2E ํ
์คํธํ ๋ ์ ์ฉํ๋ค๊ณ ํ๋ค.
Chrome ๋ธ๋ผ์ฐ์ ์ ๊ธฐ๋ฅ์ ๊ทธ๋๋ก ๊ฐ์ง๊ณ ์์ง๋ง, ์ค์ ๋ก ํ๋ฉด์ ๊ทธ๋ฆฌ์ง ์๊ณ ๋ด๋ถ์ ์ผ๋ก๋ง ์คํ๋๋ ๋ชจ๋
์ฆ, GUI -> CLI
Puppeteer
Google์์ ๊ฐ๋ฐํ Node.js์ฉ Headless Chrome ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Puppeteer ๋ Headless Chrome์ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ์ ์ดํ์ฌ ์น ํฌ๋กค๋ง, ์คํฌ๋ฆฐ์ท, E2E ํ
์คํธ ๋ฑ ๋ค์ํ ์์
์ ์๋ํํ ์ ์๋ค.
Chrome DevTools Protocol
์ ์ฌ์ฉํ์ฌ Chrome ๋ธ๋ผ์ฐ์ ์ ์ธ์คํด์ค๋ฅผ ์ ์ดํ๋ฉฐ, ์ด๋ฅผ ํตํด Chrome ๋ธ๋ผ์ฐ์ ์ ๋ชจ๋ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๋ค.
E2E ํ
์คํธ ๋๊ตฌ๋ก ๋ณด๋ค ๊ฐ๋จํ๊ณ ํ
์คํธ ์ฝ๋ ์์ฑ์ ๋น๊ฐ๋ฐ์๋ ์ฝ๊ฒ ์ดํดํ ์ ์๋๋ก ์ง๊ด์ ์ธ API๋ฅผ ์ ๊ณตํ๋ค.
์ฅ์
๊ฐ๋
์ฑ์ด ๋์ DSL(Domain Specific Language)์ ์ ๊ณตํ์ฌ ํ
์คํธ ์ฝ๋ ์์ฑ์ด ์ฉ์ดํจ
Selenium, Puppeteer, Playwright ๋ฑ, ๋ค์ํ ๋๋ผ์ด๋ฒ๋ฅผ ์ ๊ณตํ์ฌ ๋ค๋ฅธ ๋๊ตฌ๋ก ์์ฑ๋ ํ
์คํธ ์ฝ๋๋ฅผ CodeceptJS
๋ก ๋ณํ์ด ๊ฐ๋ฅํ๋ค.
Getting Started
Copy npx create-codeceptjs .
package.json
์ script๊ฐ ์๋์ผ๋ก ์ถ๊ฐ๋๋ค.
Copy "scripts": {
// ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ํ๋ฉด์ ๋์ ํ
์คํธ๋ฅผ ์คํํฉ๋๋ค. --steps: ๊ฐ ๋จ๊ณ์ ์คํ ๊ฒฐ๊ณผ๊ฐ ์์ธํ ํ์
"codeceptjs": "codeceptjs run --steps",
// ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ํ๋ฉด์ ๋์ฐ์ง ์๊ณ ํ
์คํธ๋ฅผ ์คํ
"codeceptjs:headless": "HEADLESS=true codeceptjs run --steps",
// ์น ๋ธ๋ผ์ฐ์ ์ CodeceptUI๋ฅผ ๋์ ํจ์ฌ ํธํ๊ฒ ํ
์คํธ๋ฅผ ์คํํฉ๋๋ค.
"codeceptjs:ui": "codecept-ui --app",
// demo๋ ๊ธฐ๋ฅ๊ณผ ์ฌ์ฉ๋ฒ์ ์ตํ๊ธฐ ์ํด codeceptjs๊ฐ ์ ๊ณตํ๋ ์ํ ์๋๋ฆฌ์ค๋ฅผ ์คํํ๋๊ฒ
"codeceptjs:demo": "codeceptjs run --steps -c node_modules/@codeceptjs/examples",
"codeceptjs:demo:headless": "HEADLESS=true codeceptjs run --steps -c node_modules/@codeceptjs/examples",
"codeceptjs:demo:ui": "codecept-ui --app -c node_modules/@codeceptjs/examples"
}
Copy npm uninstall @codeceptjs/examples // ์์ ์ฝ๋ ์ ๊ฑฐ
Copy npx codeceptjs init
? Where are your tests located? (./*_test.js)
# => ./tests/**/*_test.js
? What helpers do you want to use?
(Use arrow keys)
# => โฏ Playwright
? Where should logs, screenshots, and reports to be stored? (./output)
# => ./output
? Do you want localization for tests? (See https://codecept.io/translation/)
(Use arrow keys)
# => โฏ English (no localization)
? [Playwright] Base url of site to be tested (http://localhost)
# => http://localhost:8080
? [Playwright] Show browser window (Y/n)
# => Y
? [Playwright] Browser in which testing will be performed.
Possible options: chromium, firefox or webkit (chromium)
# => chromium
Creating a new test...
----------------------
? Feature which is being tested (ex: account, login, etc)
# => google
? Filename of a test (google_test.js)
# => google_test.js
์ํ ์ฝ๋
Copy Feature('Google');
Scenario('Search โCodeceptJSโ', ({ I }) => {
I.amOnPage('https://www.google.com/ncr');
I.fillField('[name="q"]', 'CodeceptJS');
I.click('Google Search');
I.see('codecept.io');
});
์คํ
Playwright
Headless Chrome์ ๊ธฐ๋ฐ์ผ๋ก ํ Puppteer์ ๊ณ์นํ๋ฉด์ ๋ ๋ง์ ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ์ง์ํ๋ค.
Copy npm i -D @playwright/test eslint-plugin-playwright
ํ๊ฒฝ์ค์
playwright.config.ts
Copy import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
retries: 0,
use: {
channel: 'chrome',
baseURL: 'http://localhost:8080', // ๋๋ฉ์ธ URL ์ค์
headless: !!process.env.CI, // CI ํ๊ฒฝ์์ headless๋ก ๋์
screenshot: 'only-on-failure', // ํ
์คํธ ์คํจํ ๋ ์คํฌ๋ฆฐ์ท์ ์ฐ์ด ๋จ๊น
},
};
export default config;
tests/.eslintrc.js
Copy module.exports = {
env: {
jest: false, // jest๋ฅผ ์ฌ์ฉํ์ง ์๊ณ playwright์์ ์ ๊ณตํ๋ api๋ฅผ ์ฌ์ฉํ๋ฏ๋ก false
},
extends: ['plugin:playwright/playwright-test'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
}
jest.config.js
E2E ํ
์คํธ๋ .spec, ๋จ์ ํ
์คํธ๋ .test๋ก ๊ตฌ๋ถํ์ฌ ๋จ์ํ
์คํธ ์คํ์ E2E ํ
์คํธ๋ ์คํํ์ง ์๋๋ก ์ค์
Copy module.exports = {
//...
testMatch: ['**/*.test.{ts,tsx}', '!**/*.spec.{ts,tsx}'],
// ๋๋
testPathIgnorePatterns: ['<rootDir>/e2e/'],
};
ํ
์คํธ ์ฝ๋ ์์
UI๊ฐ ๋ณต์กํด์ง๋ฉด ์ฌ์ฉ์ ๊ด์ ์์ ํน์ ์์๋ฅผ ์ง์ ํ๋ ๊ฒ์ด ์์ง์ ์ด๋ ค์ด๋ฐ ์ถํ์ ์๊ฒ๋๋ฉด ํด๋น ๋ด์ฉ์ ๋ฐ์ํด์ ๋ค์ ์์ฑํ ์์
Copy import { test, expect } from '@playwright/test';
test('Filter Food', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: '์๋น ๊ฒ์์ฐฝ' })).toBeVisible();
const searchInput = page.getByLabel('๊ฒ์');
await searchInput.fill('๋ฉ๋ฆฌ๊น๋ฐฅ');
await expect(page.getByText('์ ์ก๊น๋ฐฅ')).toBeVisible();
await searchInput.fill('๋ฉ๊ฐ๋ฐ์ ');
await expect(page.getByText('์ง์ฅ๋ฉด')).toBeVisible();
await searchInput.fill('์๋ฌด๊ฑฐ๋');
await expect(page.getByText('์ง์ฅ๋ฉด')).toBeHidden();
const radioBtn = page.getByRole('radio', { name: 'ํ์' });
await radioBtn.click();
await expect(page.getByText('๋ฉ๋ฆฌ๊น๋ฐฅ')).toBeVisible();
await expect(page.getByText('๋ฉ๊ฐ๋ฐ์ ')).toBeHidden();
});
test('Cart', async ({ page }) => {
await page.goto('/');
const addBtn = await page.waitForSelector('button[name="#์ง์ฅ๋ฉด"]');
await addBtn.click();
await addBtn.click();
await expect(page.getByText('ํฉ๊ณ: 8000์ ์ฃผ๋ฌธ')).toBeVisible();
const removeBtn = page.getByRole('button', { name: '์ง์ฅ๋ฉด์ ๊ฑฐ' });
await removeBtn.click();
await expect(page.getByText('ํฉ๊ณ: 0์ ์ฃผ๋ฌธ')).toBeVisible();
});
test('When order complete display receipt ', async ({ page }) => {
await page.goto('/');
const addBtn = await (page.waitForSelector('button[name="#์ง์ฅ๋ฉด"]'));
await addBtn.click();
const orderBtn = page.getByRole('button', { name: '์ฃผ๋ฌธํ๊ธฐ' });
await orderBtn.click();
await expect(page.getByRole('table', { name: '์์์ฆ' })).toBeVisible();
});
test('Receipts disappear after 5 seconds', async ({ page }) => {
await page.goto('/');
const addBtn = await page.waitForSelector('button[name="#์ง์ฅ๋ฉด"]');
await addBtn.click();
const orderBtn = page.getByRole('button', { name: '์ฃผ๋ฌธํ๊ธฐ' });
await orderBtn.click();
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(5000);
await expect(page.getByRole('table', { name: '์์์ฆ' })).toBeHidden();
await expect(page.getByRole('heading', { name: '[์์์ฆ ๋์ค๋ ๊ณณ]' })).toBeVisible();
});