프롤로그
과거 취준생 시절에 여러 공고에서 테스트 코드를 작성해본 경험이 있는 사람이 우대사항으로 굉장히 많이 올라왔어서 알고는 있어야 되겠다는 생각으로 얕게 시도해 본 적이 있습니다. 이 때 이후로 테스트 코드를 사용하는 환경에는 없었지만 필요성에 대해서는 인지하고 있었던 것 같아요. 코드의 품질 이런 것 보다도 리팩토링 과정에서 생길 수 있는 버그를 사전 체크 할 수 있다고 생각했기 때문입니다.
팀 내부적으로 테스트 코드를 도입하긴 어려운 시점이다보니 개인적으로 공부해보고 싶었던 Playwright의 공식문서를 통해 정리해 보도록 하겠습니다. 저도 처음 시작하는 단계이다 보니 공식문서가 제안하는 의도와 다르게 해석하는 경우가 있을 순 있으니 혹시나 읽으시는 분이 계시다면 참고해주세요. 🙂
Playwright 초입
Playwright는 end to end 테스트에서 요구되는 요구사항들을 수용하기 위해 만들어진 라이브러리입니다. 이 라이브러리는 최신 렌더링 엔진인 크로미움, 웹킷, 파이어폭스(게코)를 지원하는 걸로 보입니다.
Docs를 보면 Configuration에 대한 설명이 제일 앞에 있기 때문에 이 옵션에 대해서 짚고 넘어가야 할 것 같습니다. 대부분의 라이브러리는 Configuration을 이용해서 라이브러리의 기능을 커스텀 할 수 있습니다. Configuration을 지원하는 많은 라이브러리들의 경우 생각보다 다양한 기능을 제공하기 때문에, 뭔가 필요한게 생기면 옵션이 있는지 확인하는 것도 좋은 것 같습니다. 대부분의 유저가 필요하다고 느끼는 기능이 있다면 메인테이너는 이를 웬만하면 놓치지 않기 때문이겠죠?
Configuration
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // 설정 파일(config 파일)이 위치한 디렉토리를 기준으로 상대적인 경로를 지정한다는 의미입니다 testDir: 'tests', // 모든 테스트를 병렬로 진행할 지에 대한 여부 fullyParallel: true, // test.only로 표시된 테스트가 있다면 종료할 지 여부 forbidOnly: !!process.env.CI, // 테스트 마다 얼마 만큼의 재시도를 할 retries: process.env.CI ? 2 : 0, // 병렬적으로 테스트를 진행할 때 사용할 프로세스 개수입니다. 백분율로 설정하면 내 컴퓨터의 CPU 코어 개수를 기반으로 설정됩니다. // 만약 병렬 테스트를 진행한다면 효율적인 코어 개수를 설정하면 속도를 더 빠르게 할 수 있겠습니다. // 적용해본 적이 없어 섣부른 판단일 수 있지만, 테스트를 자주 돌리는 환경이라면 옵션을 잘 설정해야 될 것 같습니다. workers: process.env.CI ? 1 : undefined, // 리포터를 생성할 형태를 지정 reporter: 'html', use: { // `await page.goto('/')`와 같이 사용했을 때 베이스가 될 URL baseURL: 'http://127.0.0.1:3000', // 실패한 테스트를 재시도 할 때, 트레이스를 수집합니다. trace: 'on-first-retry', }, // 주 브라우저 설정 projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, ], // 테스트 시작 전에 로컬 개발 서버를 실행하기 위한 커맨드와 url을 입력받습니다. // 이때 이미 사용하고 있는 서버가 있다면 재사용 할 지 여부도 선택할 수 있습니다. CI 에서는 새 서버가 떠야겠죠? webServer: { command: 'npm run start', url: 'http://127.0.0.1:3000', reuseExistingServer: !process.env.CI, }, });
위 옵션 중에서 잘 설정하고 넘어가야 하는 부분은 use, webServer 인 것으로 보이네요. 외에는 workers 옵션이 테스트 속도를 높이는 데 있어서 중요해 보입니다.
필터링
import { defineConfig } from '@playwright/test'; export default defineConfig({ testIgnore: '*test-assets', testMatch: '*todo-tests/*.spec.ts', });
필터링은 Glob 패턴과 정규식 패턴을 이용해서 테스트 파일을 매칭 또는 제외시킬 수 있습니다.
고급 옵션
import { defineConfig } from '@playwright/test'; export default defineConfig({ // 이미지, 영상, 트레이스와 같은 파일들이 저장되는 경로 outputDir: 'test-results', // 전역 셋업 파일의 경로 설정 globalSetup: require.resolve('./global-setup'), // 전역 분해 파일의 경로 설정 globalTeardown: require.resolve('./global-teardown'), // 각 테스트 마다 주어지는 시간제한 timeout: 30000, });
흠.. 아직 globalSetup과 globalTeardown은 어떤 용도인지 파악하기가 쉽지 않습니다. 저는 보통 이런 경우 예제를 보거나 테스트하면서 어떤 용도인지 파악해보곤 하기 때문에 일단 넘어가겠습니다.
테스트 작성법
아래는 공식문서에 기재된 가장 기본 예시입니다.
import { test, expect } from '@playwright/test'; test('has title', async ({ page }) => { await page.goto('https://playwright.dev/'); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/Playwright/); }); test('get started link', async ({ page }) => { await page.goto('https://playwright.dev/'); // Click the get started link. await page.getByRole('link', { name: 'Get started' }).click(); // Expects page to have a heading with the name of Installation. await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); });
테스팅 라이브러리 들은 약간 비슷비슷한 형태를 가지고 있는 것 같습니다. 제가 잠깐 경험해봤던 Jest, RTL 같은 경우도 describe, test와 같은 콜백 함수로 각 테스트를 시작하고 함수를 통해 어떤 동작을 하는지 결과를 추론했던 것 같은데 비슷한 느낌이 있습니다. 조금 특이한 점은 모든 부분에 async await 이 존재한다는 것인데요. 아마도 e2e라서 비동기적인 요청들을 마치 동기적으로 동작하게 끔 해야되는 것 같습니다.
기본 액션 함수
테스트를 작성할 때 가장 많이 사용할 기본 액션 함수로 보입니다. 당연히 한번 씩 봐야겠죠?
locator.check() // Input Checkbox를 클릭 (체크) locator.uncheck() // 반대로 체크 해제 locator.click() // element 요소를 클릭 locator.hover() // 요소에 마우스를 올렸을 때 locator.fill() // form field, input text를 채우는 것 locator.focus() // element 요소에 포커싱하기 locator.press() // 어떤 특정 키를 누르는 동작 locator.setInputFiles() // 파일을 업로드하는 동작 locator.selectOption() // 드롭다운 메뉴에서 옵션을 선택하는 동작
참 또는 거짓
액션을 통해서 진행된 행동에 대한 참, 거짓 여부를 판단하는 행위입니다. 예를 들어서 Input Checkbox를 클릭했다면 클릭 여부를 결과로 추론하는 것과 같습니다. 주로 사용되는 행동은 아래와 같습니다.
expect(locator).toBeChecked() // 체크가 되었는지 expect(locator).toBeEnabled() // 활성화 되었는지 expect(locator).toBeVisible() // 요소가 보이는지 expect(locator).toContainText() // 요소에 어떤 텍스트가 포함되어 있는지 expect(locator).toHaveAttribute() // 요소에 속성이 존재하는지 expect(locator).toHaveCount() // 요소의 길이로 주어진 값 expect(locator).toHaveText() // 요소에 어떤 텍스트가 매치되는지 (정확히) expect(locator).toHaveValue() // Input 요소에 어떤 값을 가지고 있는지 expect(page).toHaveTitle() // 페이지에 타이틀이 어떻게 되는지 expect(page).toHaveURL() // 페이지 URL이 어떻게 되는지
마무리
오늘은 Playwright에서 사용되는 Configuration과 액션 함수 그리고 참, 거짓 여부를 추론하는 방법에 대해서 정리해 보았습니다. 공식 문서의 좋은 점은 정확한 사용 방법에 대해서 알 수 있다는 점이지만, 반대로 심화된 정보를 얻는 것은 어려워서 이런 건 어떻게 습득해야 할 지 고민이 많이 되네요.
역시 학습할 때는 실무에 녹이는 게 제일 좋은 방법인 것 같기도 합니다. 읽어주셔서 감사합니다.