Playwright beforeAll 함수를 제공하고 있어서 SPA 구조로 되어 있는 웹에서는 초기에 링크 이동 후 page.goBack() 이동 시 beforeEach 대비해서 테스트 속도가 빨라지는 걸 느낄 수 있었다.(단, beforeAll에서는 page 얻어올 수 없어서 새로 만들어야하는데 그럴 경우 병렬 실행을 할 수가 없었다)
회사에 프로젝트에 추가해 본 결과 대략 5배 정도 빨리진다는 걸 알 수 있었습니다.
테스트 지원 환경
Playwright
크로미움 기반 브라우저, 파이어폭스, 사파리 지원
모바일 브라우저 제공
실험모드: 안드로이드 크롬/웹뷰, Electron 지원예정
Cypress
크로미움 기반의 브라우저, 파이어폭스 2가지만 지원
병렬 테스트 지원
Playwright
모든 테스트를 병렬로 진행
최대 4개까지 worker thread 세팅 가능
단 worker thread 지정 시 테스트 단위로 고립해서 작성이 필수!!
Cypress
모든 테스트를 직렬로 진행
단, 병렬 테스트 진행 시 유료 플랜을 가입해야지 CI/CD 환경에서 별렬 테스트를 수행 가능 → 돈을 내야해요ㅜㅜ)
많은 이벤트 지원
hover, drag 지원(cypress 미지원)
문법
지원하는 언어에 대한 순수 문법을 지원한다.(cypress에서는 자체 문법을 제공)
Playwright 소개
Playwright는 MS에서 만든 오픈소스 웹 테스트, 자동화 라이브러리 입니다. 하나의 API로 Chromium, Firefox, WebKit, 모웹(Chromium, Safari등)까지 테스트 할 수 있습니다.(Next 버전에서는 ADB를 이용한 Android Chrome, Electron )도 지원하는 걸로 보입니다)
사실, Playwright 자체는 E2E 테스트 프레임워크가 아니고, @playwright/test 프레임워크를 사용하여서 크로스 브라우징 테스트가 가능합니다. (단, 레거시 Edge나 IE 지원하지 않음)
playwright 특징
특징
하나의 API로 멀티 브라우저/플랫폼 테스트 가능
Cross-browser: 최신 모던 브라우저인 Chromium, Webkit, Firefox 지원
Cross-platform: Windows, Linux, MacOS, locally or CI, headless or headed
describe, test
describe로 테스트들을 논리적으로 나누고, test로 테스트를 작성합니다.
describe 내에서 테스트 전체 선행/후행 작업 필요 시 beforeAll, AfterEach를 사용해보고,
→ context, page 생성 시 시간이 많이 소요됨으로 전체 테스트 전에 미리 생성해두면 테스트 시간을 줄일 수 있습니다.
각 테스트 별로 선행/후행 작업 필요 시 beforeEach, afterEach를 사용해주세요.
import { expect, Page, test, BrowserContext } from '@playwright/test';test.describe('홈', () => { let browserContext: BrowserContext; let page: Page; // beforeAll hook은 최초 딱 한번 실행. initialize 작업 등을 수행 test.beforeAll(async ({ browser, contextOptions }) => { browserContext = await browser.newContext(contextOptions); }); test.beforeEach(async ({ isMobile }) => { // 페이지 생성 page = await browserContext.newPage(); await page.goto('/'); if (isMobile) { await page.locator('[aria-label="navigation-button"]').click(); await page.locator('[aria-label="career-link"]:visible').click(); } else { await page .locator( '[aria-label="desktop-navigation"] [aria-label="career-link"]:visible', ) .click(); } }); test.afterEach(async () => { await page.close(); }); test.afterAll(async () => { await browserContext.close(); }); test('02-5. Carrer 상세 화면에 대한 검증을 한다.', async () => { const element = await page.locator('main > section > ul > li a').first(); const href = (await element.getAttribute('href')) as string; element.click(); await expect(page).toHaveURL(href); const subElement = await page.locator('main section > div').first(); await expect(subElement).toBeVisible(); });});
테스트 제목
테스트 제목은 최대한 명시적으로 제목만 보아도 어떤 테스트를 수행하는 지 알 수 있어야하며, 능동형 문장으로 작성합니다. (~~된다 라는 표현은 지양하고 ~~한다 라는 표현으로 작성해요)
사소한 꿀팁
test.only
테스트 파일안에 여러 테스트 케이스들이 있는데 하나의 테스트만 수정해도 파일 내의 모든 테스트 케이스들이 실행됩니다. 작성 중인 테스트 케이스만 실행되도록 test.only 를 사용할 수 있습니다.
test.skip
실행 불가능한 테스트 케이스지만 테스트 목록에는 넣고 싶을 때 test.skip을 사용할 수 있습니다. 최종 결과에 pending으로 집계됩니다.
Selector
playwright에 selector는 쿼리 우선순위를 기반으로 제공되며, 해당 기능이 안될 locator를 이용해서 자유롭게 엘리먼트에 접근이 가능합니다.
쿼리 우선 순위
기본적으로 Assertion이 에러가 날 경우 테스트가 종료되지만, Soft Assertions 적용 시 테스트가 종료되지 않고 실패에 대해서 마킹만 해주게 됩니다.
// Make a few checks that will not stop the test when failed...await expect.soft(page.getByTestId('status')).toHaveText('Success');await expect.soft(page.getByTestId('eta')).toHaveText('1 day');// ... and continue the test to check more things.await page.getByRole('link', { name: 'next page' }).click();await expect .soft(page.getByRole('heading', { name: 'Make another order' })) .toBeVisible();
Polling
expect.poll사용하여서 비동기적으로 기다릴 때 사용하게 됩니다.(API 응답을 할 경우 대부분 사용)
브라우저 인스턴스를 만드는 데 비용이 많이 들기 때문에 Playwright는 단일 인스턴스가 여러 브라우저 컨텍스트를 통해 수행할 수 있는 작업을 극대화하도록 설계되었다.
브라우저 컨텍스트는 브라우저 인스턴스 내에서 분리된 유사 세션이다. 만드는데 빠르고 비용이 적다. 각 테스트를 새로운 브라우저 컨텍스트에서 실행하여 브라우저 상태를 테스트 간에 분리하는 것이 좋다.
페이지는 다시 1개 이상의 프레임 객체를 가질 수 있다. 각 페이지에는 메인 프레임이 있고 페이지 레벨 상호작용(클릭 등)이 메인 프레임에서 작동하는 것으로 가정한다. iframe 태그와 함께 추가 프레임을 가질 수 있다.
프레임을 이용해서 프레임 내부에 있는 엘리먼트를 가져오거나 조작할 수 있다. 예제에서는 프레임을 가져오는 방법들과 프레임 내부의 엘리먼트와 상호작용하는 방법을 보여준다.
input, textarea, [contenteditable] 엘리먼트를 포커스 & 텍스트를 바로 입력할 경우 호출
locator.fill() 대체해서 사용 가능
// Text inputawait page.getByRole('textbox').fill('Peter');// Date inputawait page.getByLabel('Birth date').fill('2020-02-02');// Time inputawait page.getByLabel('Appointment time').fill('13:15');// Local datetime inputawait page.getByLabel('Local time').fill('2020-03-02T05:15');
Type characters → real keyboard type
입력 엘리먼트에 실제 키보드 입력이 되는것처럼 동작
locator.type() 대체해서 사용 가능
// Type character by characterawait page.locator('#area').type('Hello World!');
엘리먼트를 선택하거나 단축키를 생성하기 위한 동작
keyboardEvent.key 이벤트를 사용이 가능
locator.press() 대체 가능
// Hit Enterawait page.getByText('Submit').press('Enter');// Dispatch Control+Rightawait page.getByRole('textbox').press('Control+ArrowRight');// Press $ sign on keyboardawait page.getByRole('textbox').press('$');// <input id=name>await page.locator('#name').press('Shift+A');// <input id=name>await page.locator('#name').press('Shift+ArrowLeft');
input[type=checkbox], input[type=radio] and [role=checkbox] 엘리먼트에 대해서 체크/미체크 세팅, 얻어올 경우 호출
locator.setChecked() 대체해서 사용 가능
// Check the checkboxawait page.getByLabel('I agree to the terms above').check();// Assert the checked stateexpect( await page.getByLabel('Subscribe to newsletter').isChecked(),).toBeTruthy();// Select the radio buttonawait page.getByLabel('XL').check();
select 엘리먼트를 단일/다중 선택 시 사용
특정 옵션에 value, label 선택이 가능
locator.selectOption() 대체해서 사용가능
// Single selection matching the valueawait page.getByLabel('Choose a color').selectOption('blue');// Single selection matching the labelawait page.getByLabel('Choose a color').selectOption({ label: 'Blue' });// Multiple selected itemsawait page .getByLabel('Choose multiple colors') .selectOption(['red', 'green', 'blue']);
DOM이 접근이 가능할 때까지 대기
DOM이 보여질 때까지 대기(no empty, no display:none, no visibility:hidden)
움직임이 멈출때까지 대기(예: transition finishes)
뷰포트 안에 엘리먼트가 보여질 경우(scroll)
엘리먼트가 다른 엘리먼트에 가려지지 않을 때까지 대기
엘리먼트가 detach 된다면 다시 attach 될때까지 대기
강제클릭: 다른 엘리먼트에 의해서 가려질 경우 강제로 클릭 가능
dispatchEvent: 사용자의 입력과 상관없이 클릭 이벤트만 전달하고 싶은경우
// Generic clickawait page.getByRole('button').click();// Double clickawait page.getByText('Item').dblclick();// Right clickawait page.getByText('Item').click({ button: 'right' });// Shift + clickawait page.getByText('Item').click({ modifiers: ['Shift'] });// Hover over elementawait page.getByText('Item').hover();// Click the top left cornerawait page.getByText('Item').click({ position: { x: 0, y: 0 } });// Force Clickawait page.getByRole('button').click({ force: true });// Dispatch Clickawait page.getByRole('button').dispatchEvent('click');
input 엘리먼트 type=”file”인 경우 사용이 가능
locator.setInputFiles() 대체 가능
엘리먼트가 동적으로 생성된 경우 waitForEvent로 대기 가능(page.on(’filechooser’) 대체 가능)
// Select one fileawait page.getByLabel('Upload file').setInputFiles('myfile.pdf');// Select multiple filesawait page .getByLabel('Upload files') .setInputFiles(['file1.txt', 'file2.txt']);// Remove all the selected filesawait page.getByLabel('Upload file').setInputFiles([]);// Upload buffer from memoryawait page.getByLabel('Upload file').setInputFiles({ name: 'file.txt', mimeType: 'text/plain', buffer: Buffer.from('this is test'),});// Start waiting for file chooser before clicking. Note no await.const fileChooserPromise = page.waitForEvent('filechooser');await page.getByLabel('Upload file').click();const fileChooser = await fileChooserPromise;await fileChooser.setFiles('myfile.pdf');
locator.focus() 함수로 대체 가능
await page.getByLabel('Password').focus();
locator.dragTo() 대체 가능
lower-level 함수로 대체할 경우(locator.hover, mouse.down, mouse.move, mouse.up 사용가능)
기본적으로 테스트 실패 시 실행이 종료되지만, soft assertion 일 경우에는 종료가 되지 않는다.
실패 여부를 체크하기 위해서 test.info 사용 가능
// Make a few checks that will not stop the test when failed...await expect.soft(page.getByTestId('status')).toHaveText('Success');await expect.soft(page.getByTestId('eta')).toHaveText('1 day');// ... and continue the test to check more things.await page.getByRole('link', { name: 'next page' }).click();await expect .soft(page.getByRole('heading', { name: 'Make another order' })) .toBeVisible();// test.info//// Make a few checks that will not stop the test when failed...await expect.soft(page.getByTestId('status')).toHaveText('Success');await expect.soft(page.getByTestId('eta')).toHaveText('1 day');//// Avoid running further if there were soft assertion failures.expect(test.info().errors).toHaveLength(0);
특정 에러 메시지를 표현
expect.soft 대체 가능
await expect(page.getByText('Name'), 'should be logged in').toBeVisible();Error: should be logged in// Call log:// - expect.toBeVisible with timeout 5000ms// - waiting for "getByText('Name')"// 2 |// 3 | test('example test', async({ page }) => {// > 4 | await expect(page.getByText('Name'), 'should be logged in').toBeVisible();// | ^// 5 | });// 6 |// 동일한 코드expect.soft(value, 'my soft assertion').toBe(56);
expect.poll사용하여서 비동기적으로 기다릴 때 사용하게 됩니다.(API 응답을 할 경우 대부분 사용)
테스트 각각에 대해서 격리해서 작업(local/session storage, data, cookies등)
테스트에 특정 반복을 피해야할 경우 before and after hooks를 이용
테스트 파일 내에서 특정 URL 이동, 앱의 이부에 로그인 진행 시 before hooks 이용
import { test } from '@playwright/test';test.beforeEach(async ({ page }) => { // Runs before each test and signs in each page. await page.goto('https://github.com/login'); await page.getByLabel('Username or email address').fill('username'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Sign in' }).click();});test('first', async ({ page }) => { // page is signed in.});test('second', async ({ page }) => { // page is signed in.});
전체적인 흐름이 맞는 테스트 코드 작성(긴 테스트도 괜찮다.)
전체 앱 흐름을 테스트 할 경우 여러 작업에 대한 assertion 처리로 괜찮다.
긴 테스트를 굳이 개별 테스트로 나눌 경우 테스트 실행 속도를 늦추기 때문에 피해야 한다.
긴 테스트 에러 발생 시 종료하지 않고 에러만 표시할 경우 soft assertion 사용 가능하다.
// Make a few checks that will not stop the test when failed...await expect.soft(page.getByTestId('status')).toHaveText('Success');// ... and continue the test to check more things.await page.getByRole('link', { name: 'next page' }).click();