Playwright 테스트 작성

Playwright를 활용한 테스트 자동화는 Page Object Model(POM)을 통해 코드의 재사용성과 유지보수성을 높일 수 있습니다. 유틸리티 함수와 Fixtures 기반 Custom Command로 반복 작업을 캡슐화해 간결한 테스트 작성이 가능하며, 환경 변수로 URL을 관리하면 환경별 테스트도 쉽게 처리할 수 있습니다. Role 기반 선택자와 다국어 지원 전략은 접근성과 국제화를 고려한 테스트를 설계하는 데 유용합니다. Mock API 활용과 데이터 분리로 테스트의 확장성과 커버리지를 극대화할 수 있습니다.

Playwright 테스트 작성

첫 테스트 작성

간단한 로그인 테스트

Playwright를 사용해 간단한 로그인 테스트를 작성하려면 다음 단계를 따르면 됩니다. 여기서는 TypeScript를 기준으로 작성합니다.

1. Playwright 설치

Playwright가 설치되어 있지 않다면 아래 명령어를 사용해 설치하세요.

npm install playwright

2. 테스트 파일 작성

tests/login.spec.ts라는 파일을 생성하고 아래 코드를 작성합니다.

import { test, expect } from '@playwright/test';

test.describe('Login Test', () => {
  test('should log in with valid credentials', async ({ page }) => {
    // 1. 페이지 이동
    await page.goto('https://example.com/login');

    // 2. 이메일 및 비밀번호 입력
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'password123');

    // 3. 로그인 버튼 클릭
    await page.click('button[type="submit"]');

    // 4. 로그인 성공 확인
    await expect(page).toHaveURL('https://example.com/dashboard'); // 로그인 성공 후 이동할 URL
    await expect(page.locator('text=Welcome')).toBeVisible(); // 로그인 후 보이는 특정 텍스트 확인
  });
});

3. Playwright 설정 파일 구성 (선택 사항)

Playwright 설정 파일에서 기본 브라우저와 테스트 경로 등을 지정할 수 있습니다. playwright.config.ts 파일에 기본 설정을 추가합니다.

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30 * 1000,
  retries: 1,
  use: {
    headless: true, // GUI 표시 여부
    baseURL: 'https://example.com',
  },
});

4. 테스트 실행

테스트를 실행하려면 아래 명령어를 실행합니다.

npx playwright test

5. 결과 확인

테스트가 성공하면 Playwright가 테스트를 통과했음을 터미널에 표시합니다. 실패할 경우 실패한 이유와 디버깅을 위한 스크린샷 등을 제공합니다.

추가 팁

  1. 테스트가 실패했을 때 스크린샷을 저장하려면 설정 파일에 screenshot: 'only-on-failure'를 추가하세요.
  2. 비밀번호 등의 민감한 데이터를 환경 변수로 관리하세요.
  3. await page.waitForSelector() 등을 활용하여 특정 요소가 로드될 때까지 기다릴 수 있습니다.

이 테스트를 기반으로 Playwright의 다양한 기능을 확장해볼 수 있습니다.


playwright.config.ts 파일에 screenshot: 'only-on-failure' 옵션을 추가하려면 아래와 같이 작성할 수 있습니다.

수정된 playwright.config.ts

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30 * 1000, // 각 테스트 타임아웃 설정 (ms 단위)
  retries: 1, // 테스트 실패 시 재시도 횟수
  use: {
    headless: true, // GUI 표시 여부
    baseURL: 'https://example.com', // 기본 URL 설정
    screenshot: 'only-on-failure', // 테스트 실패 시에만 스크린샷 저장
    trace: 'retain-on-failure', // 실패 시 상세 트레이스도 저장
  },
});

주요 옵션 설명

  • screenshot: 'only-on-failure': 테스트가 실패했을 경우에만 스크린샷을 자동으로 저장합니다.
  • trace: 'retain-on-failure': Playwright의 Trace Viewer를 사용해 테스트 실행의 세부 로그를 저장합니다.
  • testDir: 테스트 파일들이 위치한 폴더를 지정합니다.

실행 후 결과

  1. 테스트가 실패하면, Playwright는 자동으로 test-results 디렉토리에 스크린샷을 저장합니다.
  2. 실패한 테스트와 관련된 파일들은 playwright show-trace 명령으로 확인할 수 있는 트레이스 파일과 함께 제공됩니다.

실행 명령어

테스트 실행 후 결과를 확인하려면 아래 명령어를 실행합니다.

npx playwright test

스크린샷 파일과 트레이스 파일을 사용해 디버깅 과정을 더 효과적으로 진행할 수 있습니다.


테스트 실행과 결과 확인

Playwright로 테스트를 실행하고 결과를 확인하는 방법은 간단합니다. 아래 절차에 따라 실행하고 결과를 확인하세요.

1. 테스트 실행

Playwright 테스트를 실행하려면 터미널에서 다음 명령어를 입력합니다.

npx playwright test

옵션

특정 테스트 파일 실행

npx playwright test tests/login.spec.ts

브라우저별 실행

npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit

UI를 보면서 실행 (GUI)

npx playwright test --headed

2. 테스트 결과 확인

결과 출력

테스트 실행 후, 터미널에 결과가 출력됩니다.

Running 1 test using 1 worker

✔  [chromium] › tests/login.spec.ts: Login Test › should log in with valid credentials (2s)

1 passed (2s)

테스트 실패 시

테스트가 실패하면 에러 메시지, 실패한 이유, 그리고 저장된 스크린샷, 로그, 트레이스 파일의 경로가 표시됩니다.

3. 실패한 테스트 디버깅

스크린샷 확인

테스트 실패 시 자동 저장된 스크린샷은 playwright-report 디렉토리에서 확인할 수 있습니다.

playwright-report
├── test-results/
│   ├── login.spec.ts-test-name-failed.png

Trace Viewer 사용

Playwright의 Trace Viewer를 통해 실패한 테스트 실행의 모든 단계를 확인할 수 있습니다.

  1. 트레이스 파일은 실패한 테스트와 함께 test-results 디렉토리에 저장됩니다.
  2. 트레이스 보기 명령어 실행
npx playwright show-trace test-results/<trace-file-name>.zip

Trace Viewer에서는 클릭한 버튼, 페이지 상태, 콘솔 로그 등을 포함해 테스트 실행 전체를 시각적으로 확인할 수 있습니다.

4. 테스트 보고서 생성

Playwright는 테스트 실행 후 자동으로 HTML 보고서를 생성할 수 있습니다.

  1. 보고서 명령어 실행
npx playwright show-report
  1. 브라우저에서 보고서를 열어 테스트 통계, 각 테스트의 세부 실행 결과 등을 확인할 수 있습니다.

5. 예제 실행

실행 명령어

npx playwright test tests/login.spec.ts --headed

성공 시 결과

✔ [chromium] › tests/login.spec.ts: Login Test › should log in with valid credentials (2s)

1 passed (2s)

실패 시 결과

✘ [chromium] › tests/login.spec.ts: Login Test › should log in with valid credentials (5s)
  Error: Expected page to have URL 'https://example.com/dashboard', but was 'https://example.com/error'

Error snapshot: test-results/login-test-failed.png
Trace: test-results/login-trace.zip

이 방법을 통해 테스트 실행과 결과를 직관적으로 확인할 수 있습니다. 문제가 발생하면 스크린샷과 Trace Viewer를 활용하여 디버깅을 진행하세요.


테스트 시나리오 설계

테스트 데이터 준비

Playwright 테스트에서 테스트 데이터는 중요합니다. 데이터 준비를 잘하면 테스트의 재사용성과 유지보수성이 향상됩니다. 아래에 테스트 데이터를 준비하고 관리하는 방법을 설명합니다.

1. 테스트 데이터 준비 방식

a. 하드코딩된 데이터 (간단한 테스트용)

테스트 파일 내에서 직접 데이터를 작성합니다. 초기 테스트 시 유용하지만, 유지보수 측면에서는 비효율적입니다.

const testData = {
  email: 'test@example.com',
  password: 'password123',
};

b. 외부 파일에서 데이터 불러오기

CSV, JSON 또는 YAML 파일 등 외부 파일을 활용하면 테스트 데이터를 관리하기 쉽습니다.

JSON 파일 예시 (test-data.json)

{
  "validUser": {
    "email": "test@example.com",
    "password": "password123"
  },
  "invalidUser": {
    "email": "wrong@example.com",
    "password": "wrongpass"
  }
}

테스트 파일에서 JSON 데이터 사용

import testData from './test-data.json';

test('should log in with valid credentials', async ({ page }) => {
  const { email, password } = testData.validUser;
  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', email);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('https://example.com/dashboard');
});

c. 환경 변수 사용

환경 변수를 활용하면 민감한 데이터를 안전하게 관리할 수 있습니다.

.env 파일

EMAIL=test@example.com
PASSWORD=password123

환경 변수 로드 (예: dotenv 라이브러리 사용)

npm install dotenv
import * as dotenv from 'dotenv';
dotenv.config();

const email = process.env.EMAIL;
const password = process.env.PASSWORD;

test('should log in with valid credentials', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('input[name="email"]', email!);
  await page.fill('input[name="password"]', password!);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('https://example.com/dashboard');
});

2. 동적 데이터 생성

테스트 중에 매번 다른 데이터를 사용해야 하는 경우, 동적으로 데이터를 생성할 수 있습니다.

a. 이메일 생성 예시

const generateEmail = () => `testuser+${Date.now()}@example.com`;

test('should register a new user', async ({ page }) => {
  const email = generateEmail();
  const password = 'password123';

  await page.goto('https://example.com/register');
  await page.fill('input[name="email"]', email);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('https://example.com/dashboard');
});

b. 라이브러리 활용:

faker.js 또는 chance.js와 같은 라이브러리를 사용해 임의의 데이터를 생성할 수 있습니다.

npm install @faker-js/faker
import { faker } from '@faker-js/faker';

test('should fill random user data', async ({ page }) => {
  const randomName = faker.name.firstName();
  const randomEmail = faker.internet.email();
  const randomPassword = faker.internet.password();

  await page.goto('https://example.com/register');
  await page.fill('input[name="name"]', randomName);
  await page.fill('input[name="email"]', randomEmail);
  await page.fill('input[name="password"]', randomPassword);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('https://example.com/dashboard');
});

3. 테스트 데이터 관리 전략

  • 공통 데이터: 여러 테스트에서 사용하는 데이터는 별도의 파일로 분리하여 관리합니다.
  • 시나리오별 데이터: 테스트 케이스별로 필요한 데이터를 별도로 정의합니다.
  • 데이터베이스 초기화: 데이터베이스가 필요한 경우, 테스트 실행 전후로 초기화 스크립트를 작성합니다.

4. 데이터베이스 초기화 예시

a. Playwright Test Hooks 사용

beforeEachbeforeAll에서 데이터베이스를 초기화합니다.

test.beforeEach(async () => {
  // 데이터베이스 초기화 로직
  await initializeDatabase();
});

b. REST API를 통한 데이터 준비:

API 호출을 통해 테스트 데이터를 사전에 준비합니다.

await page.request.post('https://example.com/api/setup', {
  data: { email: 'test@example.com', password: 'password123' },
});

이러한 방식으로 테스트 데이터를 준비하면 유지보수성과 확장성이 크게 향상됩니다. 테스트 환경에 적합한 데이터 준비 방식을 선택해 적용하세요.


Playwright 테스트에서 .env 파일을 사용하여 서버 환경(예: 개발, QA, 상용)을 구분하고 실행하려면 dotenv 라이브러리를 활용하면 됩니다. 아래에 단계별로 설정하는 방법을 설명합니다.

1. .env 파일 준비

각 환경에 맞는 .env 파일을 생성합니다. 예를 들어, 개발, QA, 상용 환경에 대해 각각 .env.development, .env.qa, .env.production 파일을 작성합니다.

.env.development

BASE_URL=https://dev.example.com
EMAIL=devuser@example.com
PASSWORD=devpassword

.env.qa

BASE_URL=https://qa.example.com
EMAIL=qauser@example.com
PASSWORD=qapassword

.env.production

BASE_URL=https://prod.example.com
EMAIL=produser@example.com
PASSWORD=prodpassword

2. dotenv 설치

dotenv 라이브러리를 설치합니다.

npm install dotenv

3. Playwright 설정 파일 수정

playwright.config.ts 파일에서 환경 변수 파일을 로드하도록 설정합니다.

import { defineConfig } from '@playwright/test';
import * as dotenv from 'dotenv';
import * as path from 'path';

// 실행 환경 선택 (기본값: development)
const ENV = process.env.ENV || 'development';
dotenv.config({ path: path.resolve(__dirname, `.env.${ENV}`) });

export default defineConfig({
  testDir: './tests',
  timeout: 30 * 1000,
  retries: 1,
  use: {
    baseURL: process.env.BASE_URL, // .env에서 가져온 BASE_URL
    headless: true,
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
  },
});

4. 테스트 파일에서 환경 변수 사용

테스트 파일에서 환경 변수 값을 사용할 수 있습니다.

import { test, expect } from '@playwright/test';

test('Login Test', async ({ page }) => {
  const email = process.env.EMAIL!;
  const password = process.env.PASSWORD!;

  // 서버 URL로 이동
  await page.goto('/login'); // baseURL이 자동으로 적용됨

  // 로그인 데이터 입력
  await page.fill('input[name="email"]', email);
  await page.fill('input[name="password"]', password);

  // 로그인 버튼 클릭
  await page.click('button[type="submit"]');

  // 로그인 성공 여부 확인
  await expect(page).toHaveURL('/dashboard');
});

5. 실행 명령어

테스트 실행 시 환경을 지정하기 위해 ENV 변수를 설정합니다. 기본값은 development입니다.

개발 환경 실행

ENV=development npx playwright test

QA 환경 실행

ENV=qa npx playwright test

상용 환경 실행

ENV=production npx playwright test

6. 환경 변수 확인 및 디버깅

환경 변수가 제대로 로드되지 않는 경우, console.log(process.env)를 사용하여 환경 변수를 출력하고 확인할 수 있습니다.

7. 동적 환경에 따른 테스트 실행 예시

테스트 실행 중 동적으로 환경 변수를 변경하고 싶다면, Playwright의 CLI 옵션과 조합하여 활용할 수도 있습니다.

ENV=qa npx playwright test --project=chromium

이렇게 .env 파일을 활용하면 환경별 설정을 쉽게 관리하고 실행할 수 있으며, 개발, QA, 상용 환경에 맞는 테스트를 보다 효율적으로 수행할 수 있습니다.


Windows 환경에서 ENV=qa와 같은 명령어를 실행하면 에러가 발생할 수 있습니다. 이는 Windows의 명령 프롬프트(cmd) 또는 PowerShell이 환경 변수를 설정하는 방식이 다르기 때문입니다. Windows에서는 set 명령을 사용해야 합니다.

아래에 Windows에서 Playwright 테스트 실행 시 환경 변수를 설정하는 방법을 안내합니다.

1. PowerShell에서 환경 변수 설정

PowerShell에서는 환경 변수를 ENV로 설정한 뒤 실행할 수 있습니다.

$env:ENV="qa"; npx playwright test --ui

2. Command Prompt(cmd)에서 환경 변수 설정

Command Prompt에서는 set 명령어를 사용하여 환경 변수를 설정합니다.

set ENV=qa && npx playwright test --ui

3. OS에 관계없이 실행 스크립트 작성

OS에 따라 다르게 명령어를 실행하고 싶다면, package.jsonscripts 섹션에 환경 변수 설정을 포함하여 스크립트를 작성할 수 있습니다.

예시

{
  "scripts": {
    "test:ui:dev": "cross-env ENV=development npx playwright test --ui",
    "test:ui:qa": "cross-env ENV=qa npx playwright test --ui",
    "test:ui:prod": "cross-env ENV=production npx playwright test --ui"
  }
}

여기서 cross-env는 OS와 무관하게 환경 변수를 설정할 수 있도록 도와줍니다.

cross-env 설치

npm install --save-dev cross-env

실행 명령어

npm run test:ui:qa

4. 환경 변수 문제 해결

테스트 중 환경 변수가 제대로 설정되었는지 확인하려면, process.env를 출력하는 코드를 테스트 파일에 추가하세요.

console.log('ENV:', process.env.ENV);
console.log('BASE_URL:', process.env.BASE_URL);

이 방법을 통해 Windows 환경에서도 Playwright 테스트를 문제없이 실행할 수 있습니다. PowerShell, Command Prompt, 또는 스크립트를 사용하는 방법 중 편리한 것을 선택하세요!


테스트 흐름 설계

Playwright를 활용한 테스트 흐름 설계는 테스트의 목적, 범위, 데이터 준비, 실행 순서, 환경, 그리고 결과 분석을 포함하는 체계적인 접근법이 필요합니다. 아래에 단계별로 테스트 흐름 설계 방법을 안내합니다.

1. 테스트 목적 정의

테스트를 설계하기 전에 테스트의 목표를 명확히 설정하세요.

  • 기능 테스트: 특정 기능(예: 로그인, 회원가입)이 제대로 작동하는지 확인.
  • UI 테스트: 버튼, 링크, 입력 필드 등 UI 요소가 올바르게 작동하는지 확인.
  • 통합 테스트: 여러 모듈이 서로 올바르게 동작하는지 확인.
  • 성능 테스트: 페이지 로딩 시간, 서버 응답 시간 확인.

2. 테스트 시나리오 정의

테스트가 커버해야 할 주요 시나리오를 정의합니다.

로그인 테스트

  • 정상 흐름: 유효한 사용자 정보로 로그인 시 성공.
  • 예외 흐름: 잘못된 비밀번호, 빈 입력값 등으로 오류 메시지 확인.
  • 보안 흐름: 로그인 상태에서 세션 만료 또는 재로그인 요청 처리.

주문 프로세스 테스트

  1. 상품 추가 → 장바구니 → 결제.
  2. 잘못된 결제 정보 처리 흐름.

3. 테스트 단계 설계

테스트는 일반적으로 아래 단계를 따릅니다.

a. 준비 단계

  • 테스트 데이터 준비 (환경 변수, 외부 JSON 파일, API 호출 등).
  • 테스트 환경 설정 (브라우저, 헤드리스 모드, 네트워크 설정 등).
  • 초기 상태 확인 (로그아웃, 특정 페이지로 이동).

b. 실행 단계

  • 페이지 방문 및 요소 상호작용 (클릭, 입력, 제출 등).
  • 필요한 테스트 조건을 충족하기 위한 시뮬레이션.

c. 검증 단계

  • 요소 상태 확인 (toBeVisible, toHaveText, toHaveURL 등).
  • 성공 또는 실패 메시지 확인.
  • API 응답 검증.

d. 정리 단계

  • 세션 종료 또는 데이터 정리.
  • 로그 파일 작성 또는 스크린샷 저장.

4. 테스트 데이터 준비

테스트 데이터는 유연하고 반복 가능해야 합니다.

  • JSON 파일로 입력 데이터를 분리.
  • 임의 데이터 생성 라이브러리 활용 (예: Faker.js).
  • API 호출을 통해 사전 상태를 설정.
const testData = {
  validUser: { email: 'test@example.com', password: 'password123' },
  invalidUser: { email: 'wrong@example.com', password: 'wrongpass' },
};

5. 테스트 파일 구조

테스트 파일은 이해하기 쉽게 설계해야 합니다.

tests/
├── auth/
│   ├── login.spec.ts
│   ├── signup.spec.ts
├── product/
│   ├── search.spec.ts
│   ├── cart.spec.ts
└── order/
    ├── checkout.spec.ts

6. 테스트 실행 순서

테스트 실행 순서는 독립적이어야 하지만, 경우에 따라 특정 순서를 따를 수도 있습니다.

  • 독립 테스트: 각 테스트는 다른 테스트에 의존하지 않음.
  • 종속 테스트: 예를 들어, 주문 흐름은 로그인이 선행되어야 함.

Playwright는 test.describetest.beforeEach를 활용해 테스트를 그룹화하거나 공유 논리를 추가할 수 있습니다.

test.describe('Login Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/login');
  });

  test('should log in with valid credentials', async ({ page }) => {
    // 테스트 로직
  });

  test('should show error with invalid credentials', async ({ page }) => {
    // 테스트 로직
  });
});

7. 테스트 환경 설정

테스트를 실행할 환경(개발, QA, 상용)을 분리하여 관리하세요.

  • Playwright Config: 환경별 baseURL 및 데이터 설정.
  • .env 파일로 환경 변수 관리.

8. 테스트 성공 및 실패 기준

각 테스트 케이스에 대해 성공 및 실패 조건을 정의합니다.

  • 페이지 URL 확인: await expect(page).toHaveURL('/dashboard');
  • 특정 텍스트 존재 확인: await expect(page.locator('text=Success')).toBeVisible();
  • 네트워크 요청 확인: page.on('response', ...)를 활용.

9. 테스트 결과 보고

테스트 결과를 저장하고 분석하기 위해 Playwright의 보고 기능을 활용하세요.

HTML 보고서 생성

npx playwright test --reporter=html

트레이스 파일 활용

npx playwright show-trace test-results/trace.zip

10. 예제 흐름

로그인 후 대시보드 접근 테스트

import { test, expect } from '@playwright/test';

test.describe('Dashboard Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/login');
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('https://example.com/dashboard');
  });

  test('should display user data on dashboard', async ({ page }) => {
    const userName = await page.locator('.user-name').textContent();
    expect(userName).toBe('John Doe');
  });

  test('should show error for unauthorized access', async ({ page }) => {
    await page.goto('https://example.com/admin');
    await expect(page).toHaveText('Access Denied');
  });
});

11. 결론

  • 테스트 흐름 설계는 준비(데이터 및 환경 설정) → 실행(시뮬레이션) → 검증(기능 확인) → 정리(테스트 데이터 초기화)의 단계로 구성됩니다.
  • 테스트 데이터와 환경을 분리하여 유지보수성을 높이고, Playwright의 강력한 기능을 활용해 간결하면서도 효과적인 테스트 흐름을 만드세요.

요소 선택 전략

CSS Selectors와 XPath

Playwright에서 CSS SelectorXPath는 HTML 요소를 선택하는 데 사용됩니다. 두 가지 방법은 각각 장단점이 있으며, Playwright에서는 주로 CSS Selectors가 권장됩니다. 아래에서 CSS Selector와 XPath의 차이점, 사용법, 그리고 Playwright에서의 활용 방안을 설명합니다.

1. CSS Selector

CSS Selector는 HTML 문서에서 스타일을 적용할 때 사용하는 방식과 동일한 문법을 사용하여 요소를 선택합니다.

장점

  • 간결하고 가독성이 좋음.
  • 대부분의 Playwright 기능에서 기본적으로 지원.
  • 최신 브라우저 표준과 호환성이 뛰어남.

사용 예시

a. 태그 선택

input

모든 <input> 요소를 선택합니다.

b. ID 선택

#username

id="username"인 요소를 선택합니다.

c. 클래스 선택

.button

class="button"인 요소를 선택합니다.

d. 속성 선택

input[type="text"]

type="text"<input> 요소를 선택합니다.

e. 계층 구조 선택

div > span

<div>직접 자식<span>을 선택합니다.

div span

<div>모든 자손 <span>을 선택합니다.

f. 동적 상태 선택

button:enabled

활성화된 <button>을 선택합니다.

Playwright에서 사용

await page.click('input[type="text"]');
await page.fill('#username', 'testuser');

2. XPath

XPath는 XML 문서에서 노드를 선택하기 위한 쿼리 언어로, HTML에서도 활용됩니다.

장점

  • 복잡한 구조나 계층적 관계를 다루기에 적합.
  • 요소의 정확한 위치를 지정할 수 있음.

사용 예시

a. 절대 경로

/html/body/div/span

문서 루트에서 시작해 특정 경로를 따라갑니다.

b. 상대 경로

//div/span

모든 <div> 아래에 있는 <span>을 선택합니다.

c. 속성 선택

//input[@type="text"]

type="text" 속성을 가진 <input> 요소를 선택합니다.

d. 텍스트 기반 선택

//button[text()="Submit"]

텍스트가 Submit<button> 요소를 선택합니다.

e. 포함된 텍스트

//button[contains(text(), "Log")]

텍스트에 Log를 포함하는 <button>을 선택합니다.

f. 계층적 관계

//div/*[1]

<div>의 첫 번째 자식을 선택합니다.

Playwright에서 사용

XPath를 사용할 때는 page.locator() 또는 page.$()에서 'xpath=' 접두사를 붙여 사용합니다.

await page.click('xpath=//button[text()="Submit"]');
await page.fill('xpath=//input[@id="username"]', 'testuser');

3. CSS Selector와 XPath의 비교

특징 CSS Selector XPath
가독성 간결하고 읽기 쉬움 비교적 길고 복잡할 수 있음
표준 지원 모든 브라우저 표준에서 지원됨 브라우저에서 지원되지만 복잡한 문법
복잡한 구조 제한적 (계층적 관계 표현이 약함) 계층적 관계 표현에 강함
속도 더 빠름 (브라우저에서 최적화됨) 약간 느림 (엔진 처리 속도 차이)
Playwright 기본 기본적으로 지원 'xpath=' 접두사 필요

4. Playwright에서의 활용 전략

CSS Selector 추천

  • 요소 선택이 간단한 경우 (ID, 클래스, 속성 등).
  • Playwright는 CSS Selectors를 기본적으로 지원하고 빠르기 때문에 가능한 한 사용 권장.

XPath 활용

  • 요소가 고유한 ID나 클래스가 없을 때.
  • 텍스트 기반으로 요소를 선택하거나, 복잡한 계층 관계를 다뤄야 할 때.
  • 예: 다국어 웹사이트에서 버튼 텍스트가 언어에 따라 변경되는 경우.

5. 예제

CSS Selector 기반 로그인 테스트

await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('https://example.com/dashboard');

XPath 기반 로그인 테스트

await page.goto('https://example.com/login');
await page.fill('xpath=//input[@id="username"]', 'testuser');
await page.fill('xpath=//input[@id="password"]', 'password123');
await page.click('xpath=//button[text()="Login"]');
await expect(page).toHaveURL('https://example.com/dashboard');

6. 결론

  • CSS Selector는 단순하고 빠른 요소 선택에 적합하며, Playwright에서 기본적으로 권장됩니다.
  • XPath는 복잡한 관계를 다룰 때 강력하지만, 필요할 때만 사용하는 것이 좋습니다.
  • 두 가지를 조합하면 Playwright 테스트의 안정성과 유연성을 높일 수 있습니다. 상황에 따라 가장 적합한 방식을 선택하세요.

Text-based Selectors

Playwright에서 Text-based Selectors는 텍스트를 기준으로 HTML 요소를 선택하는 방법입니다. 이 방식은 직관적이고 간단하며, 특정 텍스트를 포함한 요소를 찾는 데 유용합니다. 텍스트 기반 선택은 버튼, 링크, 헤더, 라벨과 같이 화면에 보이는 텍스트를 확인하거나 특정 동작을 테스트할 때 자주 사용됩니다.

1. Text-based Selectors의 기본 형식

Playwright는 요소의 텍스트를 선택할 수 있는 간단한 구문을 제공합니다.

await page.click('text="Login"');
await page.click('text=Submit');

형식

  • text="...": 정확히 일치하는 텍스트를 가진 요소를 선택합니다.
  • text=...: 부분적으로 일치하는 텍스트를 가진 요소를 선택합니다.

2. 사용 예시

a. 버튼 클릭

await page.click('text="Submit"');

Submit이라는 텍스트가 포함된 버튼을 클릭합니다.

b. 부분 텍스트 일치

await page.click('text=Log');

텍스트에 Log라는 단어가 포함된 버튼 또는 링크를 클릭합니다. 예: "Login", "Logout" 모두 매칭됩니다.

c. 대소문자 구분

Playwright의 text 셀렉터는 기본적으로 대소문자를 구분하지 않습니다. 하지만 대소문자를 구분하고 싶다면 다음과 같이 css 필터를 사용할 수 있습니다.

await page.locator('text=Login').withText('Login').click();

3. 속성과 결합한 Text-based Selectors

a. 특정 태그와 함께 사용

await page.click('button:has-text("Submit")');

텍스트가 Submit인 버튼 요소를 선택합니다.

b. 특정 클래스와 결합

await page.click('.btn-primary:has-text("Submit")');

클래스가 btn-primary이고 텍스트가 Submit인 요소를 선택합니다.

c. 텍스트와 상위 요소 결합

await page.locator('div:has-text("Welcome")').click();

<div> 태그에 텍스트 Welcome이 포함된 요소를 선택합니다.

4. 정규식을 사용한 Text-based Selectors

Playwright는 **정규식(Regex)**을 사용하여 더 강력한 텍스트 검색을 지원합니다.

a. 정규식으로 정확히 일치

await page.click('text=/^Submit$/');

Submit이라는 정확한 텍스트가 포함된 요소를 선택합니다.

b. 정규식으로 부분 일치

await page.click('text=/Log/i');

텍스트에 Log가 포함되면 선택합니다. /i 플래그는 대소문자를 구분하지 않음을 나타냅니다.

5. 텍스트 선택 우선순위

Playwright는 지정된 텍스트를 기준으로 아래 요소들에 우선적으로 매칭합니다.

  1. 정확히 일치하는 텍스트
  2. 부분적으로 일치하는 텍스트
  3. 텍스트가 포함된 상위 요소

만약 페이지에 동일한 텍스트를 가진 여러 요소가 있다면, 첫 번째로 일치하는 요소가 선택됩니다. 특정 요소를 명확히 지정하려면 locator()와 함께 필터를 추가하세요.

await page.locator('text="Login"').nth(1).click(); // 두 번째 요소 선택

6. Text-based Selectors의 활용 시나리오

a. 다국어 지원 테스트

텍스트 기반 선택은 UI가 여러 언어를 지원하는 경우 유용합니다. 예를 들어, 다국어 웹사이트에서 버튼의 텍스트가 변경되는지 확인할 수 있습니다.

await page.click('text=로그인'); // 한국어
await page.click('text=Login'); // 영어

b. 접근성 테스트

Playwright는 텍스트 기반 선택으로 접근성 라벨을 확인할 수 있습니다.

await page.click('text="Sign In"'); // 버튼 텍스트 확인
await page.locator('label:has-text("Email")'); // 라벨 텍스트 확인

7. Text-based Selectors와 다른 Selectors의 비교

Selector 유형 예시 사용 목적
Text-based text="Login" 텍스트 기반 간단한 선택
CSS Selector button.btn-primary 스타일 클래스나 ID 기반
XPath Selector //button[text()='Login'] 텍스트와 계층적 구조를 함께 사용
Attribute Selector input[name="email"] 특정 속성 값으로 선택

8. 예제 코드: Text-based Selectors 활용

import { test, expect } from '@playwright/test';

test('Login page test', async ({ page }) => {
  // 로그인 페이지 이동
  await page.goto('https://example.com/login');

  // 이메일 및 비밀번호 입력
  await page.fill('text=Email', 'test@example.com');
  await page.fill('text=Password', 'password123');

  // 로그인 버튼 클릭
  await page.click('text="Login"');

  // 대시보드로 이동 확인
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page.locator('text="Welcome, User"')).toBeVisible();
});

9. 장점과 한계

장점

  • 직관적이고 간단한 요소 선택.
  • 텍스트가 명확한 요소를 찾기에 적합.
  • 다국어 지원과 접근성 테스트에 유리.

한계

  • 텍스트가 동적으로 변경되거나, 동일한 텍스트를 가진 요소가 많을 경우 정확도가 떨어질 수 있음.
  • 속성이나 계층 구조가 중요한 경우 CSS나 XPath를 사용하는 것이 더 효과적.

Text-based Selectors는 Playwright에서 간단하고 빠르게 UI 요소를 선택하는 강력한 도구입니다. 특히, 텍스트가 중심이 되는 요소(버튼, 링크, 라벨 등)에 적합합니다. 하지만 복잡한 구조나 동적인 UI에서는 CSS 또는 XPath Selectors를 보완적으로 사용하는 것이 좋습니다.


다국어 지원 테스트는 다국어 웹사이트나 애플리케이션에서 언어별 UI와 기능이 제대로 동작하는지 확인하는 중요한 과정입니다. Playwright를 사용하면 다국어 지원 테스트를 쉽게 자동화할 수 있습니다. 아래에 다국어 지원 테스트를 설계하고 구현하는 방법을 단계별로 설명합니다.

1. 다국어 테스트 설계

다국어 테스트의 주요 목표는 다음과 같습니다.

  • UI 텍스트(버튼, 라벨, 제목 등)가 각 언어로 올바르게 표시되는지 확인.
  • 다국어 콘텐츠가 페이지 레이아웃을 깨지 않는지 확인.
  • 언어 전환 시 기능이 제대로 작동하는지 확인.
  • 기본 언어 및 언어 우선순위 설정이 올바르게 작동하는지 확인.

2. 다국어 테스트 구성 요소

a. 테스트 대상 언어 정의

지원하는 모든 언어 목록을 정의합니다.

const languages = ['en', 'ko', 'fr', 'es']; // 영어, 한국어, 프랑스어, 스페인어

b. 언어별 데이터 준비

각 언어별로 텍스트 데이터를 관리합니다. 예를 들어, JSON 파일을 활용할 수 있습니다.

translations.json

{
  "en": {
    "welcome": "Welcome",
    "login": "Login",
    "logout": "Logout"
  },
  "ko": {
    "welcome": "환영합니다",
    "login": "로그인",
    "logout": "로그아웃"
  },
  "fr": {
    "welcome": "Bienvenue",
    "login": "Connexion",
    "logout": "Déconnexion"
  }
}

3. Playwright를 활용한 다국어 테스트 코드

a. 언어 전환 테스트

아래는 다국어 언어 전환 기능을 테스트하는 코드입니다.

import { test, expect } from '@playwright/test';
import translations from './translations.json';

const languages = ['en', 'ko', 'fr']; // 테스트할 언어 목록

test.describe('Multilingual Support Test', () => {
  for (const lang of languages) {
    test(`should display correct text for ${lang}`, async ({ page }) => {
      // 언어를 URL 쿼리 매개변수로 전달 (예: /?lang=en)
      await page.goto(`https://example.com/?lang=${lang}`);

      // 페이지에서 번역된 텍스트 확인
      await expect(page.locator('h1')).toHaveText(translations[lang].welcome); // "Welcome", "환영합니다", "Bienvenue"
      await expect(page.locator('button.login')).toHaveText(translations[lang].login); // "Login", "로그인", "Connexion"
    });
  }
});

b. 언어 전환 기능 확인

언어 전환 버튼(또는 드롭다운)이 제대로 작동하는지 확인합니다.

test('Language switch functionality', async ({ page }) => {
  // 기본 언어로 페이지 열기
  await page.goto('https://example.com');

  // 언어 전환 버튼 클릭 (예: 한국어 선택)
  await page.click('button:has-text("한국어")');

  // UI 텍스트가 변경되었는지 확인
  await expect(page.locator('h1')).toHaveText('환영합니다');
  await expect(page.locator('button.login')).toHaveText('로그인');
});

c. 언어별 레이아웃 테스트

다국어 텍스트가 레이아웃을 깨트리지 않는지 확인합니다.

test.describe('Layout Test for Different Languages', () => {
  for (const lang of languages) {
    test(`should maintain layout for ${lang}`, async ({ page }) => {
      await page.goto(`https://example.com/?lang=${lang}`);

      // 특정 요소의 크기나 위치 확인
      const button = page.locator('button.login');
      const buttonBox = await button.boundingBox();

      expect(buttonBox.width).toBeGreaterThan(50); // 버튼 너비가 50px 이상인지 확인
      expect(buttonBox.height).toBeGreaterThan(20); // 버튼 높이가 20px 이상인지 확인
    });
  }
});

d. 기본 언어 설정 테스트

언어 설정이 없는 경우 기본 언어가 제대로 설정되는지 확인합니다.

test('Default language should be English', async ({ page }) => {
  // 언어 매개변수를 전달하지 않고 접속
  await page.goto('https://example.com');

  // 기본 언어 확인
  await expect(page.locator('h1')).toHaveText('Welcome');
});

4. 테스트 환경 변수로 언어 설정

Playwright 테스트 실행 시 언어를 환경 변수로 설정해 실행할 수도 있습니다.

a. .env 파일

DEFAULT_LANG=en

b. 테스트 코드

환경 변수를 읽어와 테스트에 적용합니다.

import * as dotenv from 'dotenv';
dotenv.config();

const defaultLang = process.env.DEFAULT_LANG || 'en';

test('Default language environment variable test', async ({ page }) => {
  await page.goto(`https://example.com/?lang=${defaultLang}`);
  await expect(page.locator('h1')).toHaveText(translations[defaultLang].welcome);
});

5. 다국어 테스트 시 주의할 점

1. 언어 우선순위

브라우저 언어 설정 또는 Accept-Language 헤더를 테스트합니다.

await page.context().setExtraHTTPHeaders({
  'Accept-Language': 'ko'
});
await page.goto('https://example.com');

2. UI 텍스트 길이

언어에 따라 텍스트 길이가 다를 수 있습니다. 긴 텍스트로 인해 UI가 깨지는지 확인하세요.

3. RTL 지원:

아랍어, 히브리어와 같이 오른쪽에서 왼쪽(RTL) 언어를 테스트할 경우 CSS 속성 direction을 확인합니다.

const direction = await page.locator('html').evaluate((el) => getComputedStyle(el).direction);
expect(direction).toBe('rtl');

Playwright를 사용하면 다국어 지원 테스트를 효과적으로 자동화할 수 있습니다. 텍스트 기반 선택, 환경 변수, 언어 전환 테스트 등을 활용해 언어별 UI와 기능을 철저히 검증하세요. 이를 통해 다국어 웹사이트의 품질을 보장하고 다양한 사용자 경험을 향상시킬 수 있습니다.


Role 기반 선택자

Playwright에서 Role 기반 선택자는 접근성(Accessibility) 속성을 활용해 웹 요소를 선택하는 방법입니다. 이러한 선택자는 HTML 요소의 ARIA 역할(ARIA Roles)을 기반으로 작동하며, 접근성 속성이 잘 정의된 웹 애플리케이션에서 특히 유용합니다. Role 기반 선택자는 요소의 role 속성과 연관된 ARIA 속성을 참조해 요소를 쉽게 찾을 수 있도록 도와줍니다.

1. Role 기반 선택자의 기본 형식

Role 기반 선택자는 getByRole() 메서드를 사용합니다. 이는 접근성 속성(role)과 함께 이름 또는 속성을 기준으로 요소를 선택할 수 있습니다.

기본 형식

page.getByRole(role: string, options?: { name?: string, exact?: boolean, checked?: boolean });
  • role: 선택하려는 요소의 역할(예: button, link, heading, checkbox 등).
  • name: 요소의 접근성 이름(레이블 또는 텍스트).
  • exact: 이름 매칭에서 정확히 일치할지 여부 (true일 경우 대소문자 구분).
  • 기타 옵션: checked, selected, expanded 등을 활용하여 상태 기반 요소를 선택.

2. 주요 Role 값

다음은 Playwright에서 사용할 수 있는 일반적인 Role 값입니다.

  • button: 버튼.
  • link: 링크.
  • textbox: 텍스트 입력 필드.
  • heading: 헤더 요소.
  • checkbox: 체크박스.
  • radio: 라디오 버튼.
  • combobox: 드롭다운 선택 박스.
  • dialog: 모달 또는 팝업 창.
  • menuitem: 메뉴 항목.
  • tab: 탭 UI 요소.

3. Role 기반 선택자 사용 예시

a. 버튼 선택

role="button"인 요소를 선택합니다.

await page.getByRole('button', { name: 'Submit' }).click();

b. 링크 선택

role="link"인 요소를 선택합니다.

await page.getByRole('link', { name: 'Home' }).click();

c. 입력 필드 선택

role="textbox"로 텍스트 입력 필드를 선택합니다.

await page.getByRole('textbox', { name: 'Username' }).fill('testuser');
await page.getByRole('textbox', { name: 'Password' }).fill('password123');

d. 체크박스 선택

role="checkbox"로 체크박스를 선택하고 상태를 확인합니다.

await page.getByRole('checkbox', { name: 'Remember Me' }).check();
await expect(page.getByRole('checkbox', { name: 'Remember Me' })).toBeChecked();

e. 라디오 버튼 선택

role="radio"로 라디오 버튼을 선택합니다.

await page.getByRole('radio', { name: 'Option 1' }).check();

f. 드롭다운 선택

role="combobox"로 드롭다운을 선택합니다.

await page.getByRole('combobox', { name: 'Country' }).selectOption('South Korea');

g. 모달/다이얼로그 확인

role="dialog"로 모달 창을 선택합니다.

await page.getByRole('dialog', { name: 'Confirmation' }).waitFor();

4. Role 기반 선택자와 상태 기반 속성

Role 기반 선택자는 추가적인 상태 속성을 지원합니다.

a. checked 속성

체크박스나 라디오 버튼의 상태를 확인합니다.

await page.getByRole('checkbox', { name: 'Accept Terms', checked: true });

b. selected 속성

선택된 옵션을 확인합니다.

await page.getByRole('option', { name: 'Option 1', selected: true });

c. expanded 속성

드롭다운이나 아코디언의 확장 상태를 확인합니다.

await page.getByRole('button', { name: 'More Options', expanded: true });

5. Role 기반 선택자의 장점

  • 접근성 준수: ARIA 역할을 기반으로 하여 접근성 테스트에 적합.
  • 의미 기반 선택: 요소의 시맨틱 역할을 활용하여 안정적인 선택 가능.
  • 강력한 상태 확인: 요소의 상태(checked, expanded, selected)를 쉽게 확인 가능.
  • 다국어 지원에 유리: name 옵션을 사용해 다국어 UI에서 언어별 레이블을 처리 가능.

6. Role 기반 선택자와 다른 선택자의 비교

선택자 유형 예시 특징
Role 기반 page.getByRole('button', { name: 'Login' }) 접근성 속성을 활용해 의미 기반 선택.
CSS 선택자 page.locator('button.login') 클래스나 ID를 사용해 시각적 선택.
Text 기반 page.getByText('Login') 텍스트를 기반으로 요소를 선택.
XPath 선택자 page.locator('//button[text()="Login"]') 구조적 계층을 기반으로 복잡한 선택 가능.

7. 예제 코드: Role 기반 선택자 활용

로그인 테스트

import { test, expect } from '@playwright/test';

test('Login Test with Role Selectors', async ({ page }) => {
  // 페이지 열기
  await page.goto('https://example.com/login');

  // 사용자 이름 및 비밀번호 입력
  await page.getByRole('textbox', { name: 'Username' }).fill('testuser');
  await page.getByRole('textbox', { name: 'Password' }).fill('password123');

  // 로그인 버튼 클릭
  await page.getByRole('button', { name: 'Login' }).click();

  // 대시보드로 이동 확인
  await expect(page).toHaveURL('https://example.com/dashboard');
});

8. 주의 사항

접근성 속성 부족한 페이지

  • ARIA 역할이나 aria-label 속성이 없는 경우 Role 기반 선택자가 제대로 작동하지 않을 수 있습니다.
  • 이 경우 CSS Selector나 Text Selector를 보완적으로 사용하세요.

ARIA 역할의 중복

  • 동일한 rolename을 가진 요소가 여러 개 있을 경우 첫 번째 요소가 선택됩니다.
  • locator()와 추가 필터를 조합해 특정 요소를 선택하세요.
await page.getByRole('button', { name: 'Login' }).nth(1).click();

정확한 접근성 이름 제공

  • HTML 요소에 aria-label, aria-labelledby 또는 텍스트 콘텐츠가 정확히 정의되어 있어야 합니다.

Role 기반 선택자는 접근성 테스트와 의미 기반 요소 선택에 매우 유용합니다. 특히 ARIA 속성을 잘 사용하는 웹사이트에서는 이 선택자를 사용해 테스트 안정성과 가독성을 크게 향상시킬 수 있습니다. 하지만 접근성 속성이 부족한 페이지에서는 다른 선택자와 조합해 사용하는 것이 좋습니다.


반복 작업과 리팩토링

Page Object Model

Page Object Model (POM)은 테스트 자동화에서 일반적으로 사용하는 설계 패턴으로, 웹 애플리케이션의 페이지를 객체로 표현하여 테스트 코드를 더 구조화하고 유지보수 가능하게 만듭니다. Playwright에서도 POM을 활용해 코드를 모듈화하고 재사용성을 극대화할 수 있습니다.

1. Page Object Model의 개념

POM에서는 각 페이지(또는 페이지의 섹션)를 클래스 형태로 표현합니다. 각 클래스는

  • 페이지 요소를 캡슐화하고,
  • 이 요소들과 상호작용하는 메서드를 제공합니다.

이렇게 하면 테스트 코드에서 UI 로직과 테스트 로직이 분리되어 코드의 가독성과 유지보수성이 향상됩니다.

2. Page Object Model의 장점

  • 코드 재사용성: 동일한 페이지에 대한 테스트를 여러 개 작성할 때 중복 코드가 줄어듭니다.
  • 유지보수성: 페이지 UI가 변경될 경우, 변경된 클래스 파일만 수정하면 됩니다.
  • 가독성: 테스트 시나리오 코드가 간결하고 명확하게 유지됩니다.
  • 테스트 로직과 UI 로직 분리: 테스트의 안정성과 가독성이 향상됩니다.

3. Page Object Model 구현하기

a. 디렉터리 구조

아래는 Playwright 프로젝트에서 POM을 활용할 때의 기본 디렉터리 구조 예시입니다:

project/
├── pages/
│   ├── LoginPage.ts
│   ├── DashboardPage.ts
├── tests/
│   ├── login.spec.ts
├── playwright.config.ts

b. 페이지 클래스 구현

LoginPage.ts

로그인 페이지를 위한 클래스를 작성합니다.

import { Page } from '@playwright/test';

export class LoginPage {
  private page: Page;

  // 페이지에서 사용할 요소 정의
  constructor(page: Page) {
    this.page = page;
  }

  private usernameField = () => this.page.locator('input[name="username"]');
  private passwordField = () => this.page.locator('input[name="password"]');
  private loginButton = () => this.page.locator('button[type="submit"]');
  private errorMessage = () => this.page.locator('.error-message');

  // 요소와 상호작용하는 메서드 정의
  async navigateToLoginPage(url: string) {
    await this.page.goto(url);
  }

  async login(username: string, password: string) {
    await this.usernameField().fill(username);
    await this.passwordField().fill(password);
    await this.loginButton().click();
  }

  async getErrorMessage(): Promise<string> {
    return await this.errorMessage().innerText();
  }
}

DashboardPage.ts

대시보드 페이지를 위한 클래스를 작성합니다.

import { Page } from '@playwright/test';

export class DashboardPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  private welcomeMessage = () => this.page.locator('h1');

  async getWelcomeMessage(): Promise<string> {
    return await this.welcomeMessage().innerText();
  }
}

c. 테스트 파일 작성

login.spec.ts

페이지 객체를 활용해 테스트를 작성합니다.

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

test.describe('Login Tests', () => {
  test('should log in with valid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const dashboardPage = new DashboardPage(page);

    // 로그인 페이지로 이동
    await loginPage.navigateToLoginPage('https://example.com/login');

    // 로그인 수행
    await loginPage.login('testuser', 'password123');

    // 대시보드 확인
    const welcomeMessage = await dashboardPage.getWelcomeMessage();
    expect(welcomeMessage).toBe('Welcome, Test User!');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);

    // 로그인 페이지로 이동
    await loginPage.navigateToLoginPage('https://example.com/login');

    // 잘못된 로그인 시도
    await loginPage.login('testuser', 'wrongpassword');

    // 에러 메시지 확인
    const errorMessage = await loginPage.getErrorMessage();
    expect(errorMessage).toBe('Invalid username or password.');
  });
});

4. POM에서 데이터 분리

테스트 데이터가 많거나 다국어 지원 테스트를 하려면 데이터를 별도의 파일로 분리하는 것이 좋습니다.

데이터 파일 예시 (data/loginData.json)

{
  "validUser": {
    "username": "testuser",
    "password": "password123"
  },
  "invalidUser": {
    "username": "testuser",
    "password": "wrongpassword"
  }
}

데이터 로드 방식

import loginData from '../data/loginData.json';

await loginPage.login(loginData.validUser.username, loginData.validUser.password);

5. Playwright의 Fixtures와 POM 통합

Playwright의 test.extend 기능을 사용해 POM과 테스트를 더 효율적으로 통합할 수 있습니다.

확장된 Fixture 예시

import { test as base, Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

type Pages = {
  loginPage: LoginPage;
};

export const test = base.extend<Pages>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
});

테스트에서 사용

test('should log in with valid credentials', async ({ loginPage }) => {
  await loginPage.navigateToLoginPage('https://example.com/login');
  await loginPage.login('testuser', 'password123');
});

6. POM 설계 팁

  • 단일 책임 원칙: 각 페이지 클래스는 그 페이지와 관련된 요소와 로직만 포함해야 합니다.
  • 재사용 가능한 메서드 작성: 일반적으로 사용되는 작업(예: 로그인, 폼 작성)은 메서드로 추상화하여 여러 테스트에서 재사용 가능하도록 합니다.
  • 유지보수성 고려: 페이지 요소가 변경되었을 때 테스트 코드를 수정하지 않아도 되도록 클래스 파일만 수정하면 되게 설계합니다.

Page Object Model은 Playwright 테스트를 모듈화하고, 유지보수성과 확장성을 높이는 데 큰 도움을 줍니다. POM을 사용하면 테스트 로직과 UI 로직을 분리할 수 있어 더 깔끔하고 이해하기 쉬운 코드를 작성할 수 있습니다. Playwright의 강력한 API와 함께 활용하면 복잡한 테스트 시나리오도 쉽게 관리할 수 있습니다.


Page Object Model (POM)에서 페이지 클래스(login.page.ts)는 페이지 요소만 정의하고, 실제 상호작용 동작은 별도의 유틸리티(utils.ts)로 분리하면 구조가 더욱 모듈화됩니다. 이는 UI 요소와 동작 로직을 명확히 구분하므로 유지보수성과 재사용성을 높이는 데 유리합니다.

아래는 login.page.ts와 유틸리티 분리 방식을 적용한 예제입니다.

1. 디렉터리 구조

다음과 같이 구조화합니다.

project/
├── pages/
│   ├── login.page.ts        # 페이지 요소 정의
├── utils/
│   ├── auth.utils.ts        # 로그인 관련 동작 정의
├── tests/
│   ├── login.spec.ts        # 테스트 파일
├── playwright.config.ts

2. 페이지 요소 정의

login.page.ts페이지의 요소를 정의하는 역할만 수행합니다.

login.page.ts

import { Page, Locator } from '@playwright/test';

export class LoginPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // 페이지 요소 정의
  get usernameField(): Locator {
    return this.page.locator('input[name="username"]');
  }

  get passwordField(): Locator {
    return this.page.locator('input[name="password"]');
  }

  get loginButton(): Locator {
    return this.page.locator('button[type="submit"]');
  }

  get errorMessage(): Locator {
    return this.page.locator('.error-message');
  }

  // 네비게이션 메서드 (URL 이동만 포함)
  async navigateToLoginPage(url: string): Promise<void> {
    await this.page.goto(url);
  }
}

3. 상호작용 로직 정의

auth.utils.ts로그인 동작과 관련된 유틸리티 메서드를 정의합니다. 이 파일은 LoginPage 객체를 받아서 동작을 실행합니다.

auth.utils.ts

import { LoginPage } from '../pages/login.page';

export class AuthUtils {
  private loginPage: LoginPage;

  constructor(loginPage: LoginPage) {
    this.loginPage = loginPage;
  }

  // 로그인 동작
  async login(username: string, password: string): Promise<void> {
    await this.loginPage.usernameField.fill(username);
    await this.loginPage.passwordField.fill(password);
    await this.loginPage.loginButton.click();
  }

  // 에러 메시지 가져오기
  async getErrorMessage(): Promise<string> {
    return await this.loginPage.errorMessage.innerText();
  }
}

4. 테스트 파일

login.spec.ts에서 LoginPageAuthUtils를 활용하여 테스트를 작성합니다.

login.spec.ts

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { AuthUtils } from '../utils/auth.utils';

test.describe('Login Tests', () => {
  test('should log in with valid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const authUtils = new AuthUtils(loginPage);

    // 로그인 페이지로 이동
    await loginPage.navigateToLoginPage('https://example.com/login');

    // 로그인 수행
    await authUtils.login('testuser', 'password123');

    // 대시보드 확인
    await expect(page).toHaveURL('https://example.com/dashboard');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const authUtils = new AuthUtils(loginPage);

    // 로그인 페이지로 이동
    await loginPage.navigateToLoginPage('https://example.com/login');

    // 잘못된 로그인 시도
    await authUtils.login('testuser', 'wrongpassword');

    // 에러 메시지 확인
    const errorMessage = await authUtils.getErrorMessage();
    expect(errorMessage).toBe('Invalid username or password.');
  });
});

5. 장점

a. 유지보수성

  • UI가 변경되어도 login.page.ts만 수정하면 됩니다.
  • 비즈니스 로직이 변경되어도 auth.utils.ts만 수정하면 됩니다.

b. 재사용성

  • auth.utils.ts의 메서드는 여러 테스트 파일에서 재사용 가능합니다.
  • 동일한 페이지 클래스(LoginPage)는 다른 유틸리티에서도 활용할 수 있습니다.

c. 가독성

  • 테스트 파일은 동작과 결과 검증에만 집중하므로 간결하고 읽기 쉽습니다.

6. 확장 가능성

a. 다른 유틸리티 추가

auth.utils.ts 외에도 다른 도메인별 유틸리티를 만들 수 있습니다.

  • profile.utils.ts: 프로필 관련 동작 정의.
  • cart.utils.ts: 장바구니 관련 동작 정의.

b. 공통 유틸리티 작성

테스트 환경 초기화, 데이터베이스 초기화 등 공통 작업은 별도의 유틸리티로 관리합니다.

이 구조는 테스트의 안정성과 유지보수성을 높이며, 대규모 프로젝트에서도 효율적인 테스트 코드 관리를 가능하게 합니다.


이 구조는 환경 변수에 저장된 base_url을 참조해 페이지 이동 경로를 동적으로 구성하는 형태로, Playwright의 Page Object Model(POM) 설계에 유용합니다. 이를 통해 환경별 URL 관리가 더 쉬워지고, 코드의 재사용성과 유지보수성이 향상됩니다.

1. 구현 구조

  • .env 파일: 서버 환경별로 base_url을 저장합니다.
  • navigateToPage 메서드: POM의 각 페이지 클래스에서 경로를 정의하고, base_url과 결합해 최종 URL을 생성합니다.
  • 환경 변수 로드: dotenv 라이브러리를 사용하여 .env 파일을 로드합니다.

2. 디렉터리 구조

project/
├── pages/
│   ├── login.page.ts        # LoginPage 클래스 (POM)
│   ├── dashboard.page.ts    # DashboardPage 클래스 (POM)
├── tests/
│   ├── login.spec.ts        # 테스트 파일
├── utils/
│   ├── env.utils.ts         # 환경 변수 관리 유틸리티
├── .env                     # 환경 변수 파일
├── playwright.config.ts     # Playwright 설정 파일

3. 환경 변수 설정

.env 파일

환경별 base_url을 정의합니다.

BASE_URL=https://example.com

4. 환경 변수 관리 유틸리티

환경 변수를 로드하고 사용할 수 있는 유틸리티를 작성합니다.

env.utils.ts

import * as dotenv from 'dotenv';

// .env 파일 로드
dotenv.config();

export const ENV = {
  BASE_URL: process.env.BASE_URL || 'http://localhost:3000',
};

5. POM에서 경로 관리

POM 클래스에서 경로를 path로 정의하고, base_url과 결합해 최종 URL을 생성하는 구조를 작성합니다.

login.page.ts

import { Page } from '@playwright/test';
import { ENV } from '../utils/env.utils';

export class LoginPage {
  private page: Page;

  // 경로 정의 (페이지별 path)
  private path = '/login';

  constructor(page: Page) {
    this.page = page;
  }

  // 페이지 이동
  async navigate(): Promise<void> {
    const url = `${ENV.BASE_URL}${this.path}`;
    await this.page.goto(url);
  }

  // 요소 정의
  get usernameField() {
    return this.page.locator('input[name="username"]');
  }

  get passwordField() {
    return this.page.locator('input[name="password"]');
  }

  get loginButton() {
    return this.page.locator('button[type="submit"]');
  }

  get errorMessage() {
    return this.page.locator('.error-message');
  }
}

dashboard.page.ts

import { Page } from '@playwright/test';
import { ENV } from '../utils/env.utils';

export class DashboardPage {
  private page: Page;

  // 경로 정의
  private path = '/dashboard';

  constructor(page: Page) {
    this.page = page;
  }

  // 페이지 이동
  async navigate(): Promise<void> {
    const url = `${ENV.BASE_URL}${this.path}`;
    await this.page.goto(url);
  }

  // 요소 정의
  get welcomeMessage() {
    return this.page.locator('h1');
  }
}

6. 테스트 파일 작성

login.spec.ts

POM의 navigate() 메서드를 호출하여 각 페이지로 이동합니다.

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';
import { DashboardPage } from '../pages/dashboard.page';

test.describe('Login Tests', () => {
  test('should log in with valid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const dashboardPage = new DashboardPage(page);

    // 로그인 페이지로 이동
    await loginPage.navigate();

    // 로그인 수행
    await loginPage.usernameField.fill('testuser');
    await loginPage.passwordField.fill('password123');
    await loginPage.loginButton.click();

    // 대시보드 페이지로 이동 확인
    await expect(page).toHaveURL(`${process.env.BASE_URL}/dashboard`);
    await expect(dashboardPage.welcomeMessage).toHaveText('Welcome, Test User!');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);

    // 로그인 페이지로 이동
    await loginPage.navigate();

    // 잘못된 로그인 시도
    await loginPage.usernameField.fill('testuser');
    await loginPage.passwordField.fill('wrongpassword');
    await loginPage.loginButton.click();

    // 에러 메시지 확인
    const errorMessage = await loginPage.errorMessage.innerText();
    expect(errorMessage).toBe('Invalid username or password.');
  });
});

7. 장점

  1. 환경별 URL 관리 용이: .env 파일만 수정하면 모든 테스트에서 적용됩니다.
  2. 코드 재사용성: 각 페이지 클래스에 navigate()를 정의해 동일한 로직을 재사용할 수 있습니다.
  3. 가독성: 테스트 파일에서 URL 관리 로직을 분리하여 테스트 시나리오에 집중할 수 있습니다.
  4. 확장성: 여러 환경(QA, 개발, 상용 등)을 쉽게 테스트할 수 있습니다.

8. 확장: 환경별 테스트 실행

Playwright 테스트 실행 시 환경을 선택하여 다른 .env 파일을 로드할 수 있습니다.

환경 변수 파일 예시

.env.production

BASE_URL=https://example.com

.env.development

BASE_URL=http://localhost:3000

실행 명령어

cross-env를 사용해 환경별로 테스트를 실행합니다:

npx cross-env NODE_ENV=development npx playwright test
npx cross-env NODE_ENV=production npx playwright test

이 접근 방식은 환경 변수로 base_url을 관리하고, POM에서 각 페이지 경로를 정의하여 구조적인 테스트 코드를 작성할 수 있습니다. 이로 인해 환경별 설정을 쉽게 변경할 수 있고, URL 관리가 중앙 집중화되어 유지보수가 훨씬 간편해집니다.


Custom Command 작성

Playwright에서 Custom Command를 작성하면 반복적으로 사용하는 작업(예: 로그인, 데이터 입력, 특정 요소 확인)을 재사용 가능한 메서드로 만들어 코드를 더 간결하고 유지보수하기 쉽게 만들 수 있습니다. Playwright는 기본적으로 확장 가능한 구조를 제공하므로, Fixtures 또는 유틸리티 클래스를 사용해 Custom Command를 작성할 수 있습니다.

1. Custom Command 설계 방식

Custom Command는 아래 두 가지 방식으로 구현할 수 있습니다.

  1. 유틸리티 함수: 재사용 가능한 메서드를 별도 유틸리티 파일로 관리.
  2. Fixtures 확장: Playwright의 test.extend를 사용하여 Custom Command를 확장.

2. 유틸리티 함수로 Custom Command 작성

유틸리티 함수는 특정 작업을 캡슐화하여 여러 테스트에서 재사용할 수 있습니다.

a. 디렉터리 구조

project/
├── utils/
│   ├── commands.utils.ts    # Custom Command 정의
├── tests/
│   ├── example.spec.ts      # 테스트 파일

b. Custom Command 작성

commands.utils.ts

로그인과 같은 반복 작업을 캡슐화한 함수:

import { Page } from '@playwright/test';

export async function login(page: Page, username: string, password: string): Promise<void> {
  await page.goto('https://example.com/login');
  await page.fill('input[name="username"]', username);
  await page.fill('input[name="password"]', password);
  await page.click('button[type="submit"]');
}

export async function logout(page: Page): Promise<void> {
  await page.click('button.logout');
}

c. 테스트에서 사용

example.spec.ts

Custom Command를 호출하여 테스트 작성

import { test, expect } from '@playwright/test';
import { login, logout } from '../utils/commands.utils';

test('Login and logout flow', async ({ page }) => {
  // 로그인 수행
  await login(page, 'testuser', 'password123');

  // 대시보드 확인
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page.locator('h1')).toHaveText('Welcome, Test User!');

  // 로그아웃 수행
  await logout(page);

  // 로그인 페이지로 리디렉션 확인
  await expect(page).toHaveURL('https://example.com/login');
});

3. Fixtures를 활용한 Custom Command 작성

Playwright의 test.extend 기능을 사용하면 test 객체에 Custom Command를 추가할 수 있습니다. 이는 유틸리티 메서드보다 더 간결하게 반복 작업을 처리할 수 있습니다.

a. 디렉터리 구조

project/
├── fixtures/
│   ├── custom.commands.ts   # Fixtures 확장
├── tests/
│   ├── login.spec.ts        # 테스트 파일

b. Fixtures 확장

custom.commands.ts

Custom Command를 Playwright의 Fixtures로 확장합니다.

import { test as base, Page } from '@playwright/test';

// Custom Fixtures 정의
type CustomFixtures = {
  login: (username: string, password: string) => Promise<void>;
};

// 기본 test 객체 확장
export const test = base.extend<CustomFixtures>({
  login: async ({ page }, use) => {
    // login 메서드 구현
    const login = async (username: string, password: string) => {
      await page.goto('https://example.com/login');
      await page.fill('input[name="username"]', username);
      await page.fill('input[name="password"]', password);
      await page.click('button[type="submit"]');
    };
    await use(login);
  },
});

c. 테스트에서 사용

login.spec.ts

test 객체에 확장된 login Command를 호출합니다.

import { test, expect } from '../fixtures/custom.commands';

test('Login with custom command', async ({ page, login }) => {
  // Custom Command를 사용한 로그인
  await login('testuser', 'password123');

  // 대시보드 확인
  await expect(page).toHaveURL('https://example.com/dashboard');
  await expect(page.locator('h1')).toHaveText('Welcome, Test User!');
});

4. Custom Command 설계 시 유용한 팁

  1. 유틸리티 함수 vs Fixtures
    • 유틸리티 함수는 더 간단하며 초기 프로젝트에서 적합합니다.
    • Fixturestest 객체와 통합되어 확장성이 높으며, 대규모 프로젝트에 적합합니다.
  2. 재사용 가능성
    • Custom Command는 반복적으로 사용하는 작업을 캡슐화하여 테스트 코드에서 직접 호출하지 않아도 되도록 설계하세요.
    • 예: 로그인, 세션 초기화, 데이터 정리.
  3. 매개변수화
    • Custom Command가 유연하도록 매개변수를 사용해 다양한 상황을 처리하세요.
    • 예: 로그인 시 사용자 역할(관리자, 일반 사용자) 구분.
  4. 에러 핸들링
    • Custom Command 내부에서 예외 처리를 포함하여, 테스트가 실패했을 때 명확한 로그를 제공하세요.
  5. 테스트 실행 속도 최적화
    • Custom Command 내에서 불필요한 작업(예: 불필요한 페이지 이동)을 피하고 필요한 작업만 실행하세요.

5. 확장 가능성

Custom Command는 로그인 외에도 다양한 작업에 활용할 수 있습니다.

  • 데이터 생성: API 호출로 테스트 데이터를 생성.
  • 상태 확인: 특정 요소가 화면에 표시될 때까지 대기.
  • 네트워크 설정: 네트워크 요청 가로채기(Mock API) 구현.

Custom Command는 반복적인 작업을 캡슐화하여 코드의 가독성과 유지보수성을 향상시키는 강력한 도구입니다. 작은 프로젝트에서는 유틸리티 함수로 시작하고, 점차적으로 Playwright의 Fixtures 확장을 활용해 확장성을 더하세요. 이를 통해 더 효율적이고 구조적인 테스트 코드를 작성할 수 있습니다.