Playwrite
E2E(End to End) Test
실제 사용자가 사용하는 브라우저 환경에서 애플리케이션 전체를 동작해서 기능을 확인하는 테스트
즉, 사용자가 실제로 소프트웨어를 사용했을때 플로우를 테스트하는 것
E2E Test 특징
실제 사용자 환경에서 테스트하므로 사용자의 입장에서의 경험을 직접적으로 테스트할 수 있다.
전체 시스템을 대상으로 테스트를 수행하기 때문에 각각의 모듈이나 컴포넌트가 함께 작동할 때 발생하는 문제점들을 파악 가능
외부 환경이나 네트워크를 이용한다면 조금 불안정 할 수 있다.
E2E Test Framework
Cypress
npm i -D cypressnpx cypress openCypress의 예제 코드를 살펴보면 UI 요소를 가져올 때 내부 구현 사항 (예: class)에 의존하고 있다.
testing-library는 Cypress도 지원하므로 함께 사용하면 UI 요소를 사용자 관점에서 가져올 수 있다
npm i -D @testing-library/cypressE2E 테스트에서도 마찬가지로, 네트워크 관련 의존성들은 Mock 데이터를 활용하여 테스트를 진행한다.
이는 사용자의 실제 환경과 유사한 환경에서 테스트를 수행하더라도 외부 의존성에 의해 테스트가 실패하는 것을 방지하기 위함이다.
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
npx create-codeceptjs .package.json 에 script가 자동으로 추가된다.
"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"
}npm uninstall @codeceptjs/examples 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샘플 코드
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');
});실행
npm run codeceptjsPlaywright
Headless Chrome을 기반으로 한 Puppteer을 계승하면서 더 많은 웹 브라우저를 지원한다.
npm i -D @playwright/test eslint-plugin-playwrightnpx playwright test환경설정
playwright.config.ts
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
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 테스트는 실행하지 않도록 설정
module.exports = {
//...
testMatch: ['**/*.test.{ts,tsx}', '!**/*.spec.{ts,tsx}'],
// 또는
testPathIgnorePatterns: ['<rootDir>/e2e/'],
};테스트 코드 예시
UI가 복잡해지면 사용자 관점에서 특정 요소를 지정하는 것이 아직은 어려운데 추후에 알게되면 해당 내용을 반영해서 다시 작성할 예정
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();
});Last updated