Playwright 코드 구조화와 유지보수

Playwright에서 테스트 코드를 모듈화하고 효율적으로 관리하기 위해 Page Object Model(POM)과 공통 동작 확장 전략을 활용하면 유지보수성과 확장성을 크게 향상시킬 수 있습니다. BasePage를 활용하여 공통 동작을 캡슐화하고 이를 상속하여 페이지별로 필요한 기능을 추가함으로써 코드 중복을 최소화할 수 있습니다. 또한, 공통 유틸리티를 별도로 분리하여 테스트 실행 중 자주 사용되는 동작(예: 스크린샷, 데이터 생성 등)을 관리하면 효율적인 자동화 환경을 구축할 수 있습니다.

Playwright 코드 구조화와 유지보수

생성된 코드의 모듈화

Playwright에서 자동 생성된 코드를 모듈화하면 유지보수성과 재사용성을 높일 수 있습니다. TypeScript 기반으로 작성하면 코드의 가독성이 향상되고, 타입 안정성을 확보할 수 있습니다.

1. 모듈화의 필요성

Playwright의 codegen 기능을 사용하면 하나의 긴 TypeScript 파일이 생성됩니다. 이를 그대로 사용하면 다음과 같은 문제가 발생할 수 있습니다.

  • 코드 중복 증가
  • 유지보수가 어려움
  • 테스트 시나리오 확장이 어려움

이를 해결하기 위해 Page Object Model (POM) 패턴을 적용하여 코드의 모듈화를 진행합니다.

2. 모듈화 방법

2.1. 프로젝트 폴더 구조

모듈화를 적용하면 Playwright 프로젝트 폴더 구조가 다음과 같이 정리됩니다.

playwright_project/
│── pages/
│   ├── loginPage.ts
│   ├── dashboardPage.ts
│
│── tests/
│   ├── login.test.ts
│   ├── dashboard.test.ts
│
│── utils/
│   ├── helpers.ts
│
│── playwright.config.ts  # Playwright 설정 파일
│── package.json          # 프로젝트 패키지 관리
│── tsconfig.json         # TypeScript 설정 파일

2.2. 페이지 객체 모델 적용 (POM)

(1) pages/loginPage.ts

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

export class LoginPage {
  private page: Page;
  private usernameInput = '#username';
  private passwordInput = '#password';
  private loginButton = '#login';

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

  async goto() {
    await this.page.goto('https://example.com/login');
  }

  async login(username: string, password: string) {
    await this.page.fill(this.usernameInput, username);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.loginButton);
  }

  async getUrl(): Promise<string> {
    return this.page.url();
  }
}

위 코드는 LoginPage 클래스를 정의하며, 로그인 관련 기능을 메서드화하여 재사용할 수 있도록 구성했습니다.

2.3. 테스트 코드에서 모듈화된 페이지 객체 사용

(2) tests/login.test.ts

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

test('User can log in successfully', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('testuser', 'password123');

  await expect(page).toHaveURL('https://example.com/dashboard');
});

위 테스트 코드에서는 LoginPage 클래스를 활용하여 로그인 테스트를 간결하게 작성했습니다.

2.4. 공통 기능 유틸리티 모듈화

테스트에서 자주 사용되는 기능은 utils/ 디렉터리에 모듈로 만들어 관리할 수 있습니다.

(3) utils/helpers.ts

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

export async function takeScreenshot(page: Page, filename: string) {
  await page.screenshot({ path: `screenshots/${filename}.png` });
}

(4) tests/screenshot.test.ts

import { test } from '@playwright/test';
import { takeScreenshot } from '../utils/helpers';

test('Take a screenshot of homepage', async ({ page }) => {
  await page.goto('https://example.com');
  await takeScreenshot(page, 'homepage');
});

3. Playwright 설정 (playwright.config.ts)

Playwright의 기본 설정을 적용하면 여러 브라우저에서 테스트를 병렬 실행할 수 있습니다.

(5) playwright.config.ts

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

export default defineConfig({
  use: {
    headless: false,
    viewport: { width: 1280, height: 720 },
    trace: 'on',
    screenshot: 'only-on-failure',
  },
});

위 설정은 테스트가 실패할 경우에만 스크린샷을 저장하고, trace를 활성화하여 디버깅을 용이하게 합니다.

4. 테스트 실행 방법

Playwright 테스트를 실행하기 위해 다음 명령어를 사용합니다.

# Playwright 설치
npm install @playwright/test typescript ts-node -D

# Playwright 설정 적용
npx playwright install

# 테스트 실행
npx playwright test

Playwright의 자동 생성된 코드를 TypeScript 기반으로 모듈화하면 다음과 같은 장점이 있습니다.

  1. 페이지 객체 패턴 적용 → 각 페이지를 클래스로 관리하여 재사용 가능
  2. 유틸리티 함수 정리 → 공통 기능(스크린샷 등)을 별도 파일에서 관리
  3. 폴더 구조 정리pages/, tests/, utils/ 폴더를 구분하여 코드 가독성 향상
  4. Playwright 설정 적용playwright.config.ts를 활용하여 테스트 환경 구성
  5. 테스트 실행 자동화npx playwright test로 간단하게 실행 가능

이제 Playwright 기반의 E2E 테스트 코드를 모듈화하여 유지보수가 쉬운 환경을 구축할 수 있습니다.


Page Object Model(POM) 적용하기

Page Object Model (POM)은 Playwright 테스트 코드의 모듈화를 위한 패턴으로, 웹 페이지별로 클래스를 만들어 UI 요소와 동작을 캡슐화하는 방식입니다. 이를 적용하면 테스트 코드의 재사용성유지보수성이 높아지고, 코드의 가독성이 향상됩니다.

1. Page Object Model 적용의 이점

  1. 코드 중복 감소 → 동일한 UI 요소와 동작을 여러 테스트에서 재사용 가능
  2. 유지보수 용이 → UI 변경 시 한 곳만 수정하면 모든 테스트에 반영됨
  3. 테스트 코드 가독성 향상 → 각 테스트에서 UI 요소를 직접 다루는 것이 아니라, 메서드를 호출하여 명확한 로직을 구현
  4. 테스트 구조화 → 기능별로 분리하여 논리적으로 정리된 코드 작성 가능

2. 폴더 구조

POM을 적용하면 프로젝트의 폴더 구조가 다음과 같이 정리됩니다.

playwright_project/
│── pages/               # 페이지 객체 클래스 저장
│   ├── loginPage.ts
│   ├── dashboardPage.ts
│
│── tests/               # 테스트 스크립트 저장
│   ├── login.test.ts
│   ├── dashboard.test.ts
│
│── utils/               # 공통 기능 및 유틸리티 저장
│   ├── helpers.ts
│
│── playwright.config.ts  # Playwright 설정 파일
│── package.json          # 프로젝트 패키지 관리
│── tsconfig.json         # TypeScript 설정 파일

3. 페이지 객체 클래스 작성 (POM 적용)

3.1. 로그인 페이지 객체 (pages/loginPage.ts)

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

export class LoginPage {
  private page: Page;
  private usernameInput = '#username';
  private passwordInput = '#password';
  private loginButton = '#login';

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

  /** 로그인 페이지 이동 */
  async goto() {
    await this.page.goto('https://example.com/login');
  }

  /** 사용자 로그인 */
  async login(username: string, password: string) {
    await this.page.fill(this.usernameInput, username);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.loginButton);
  }

  /** 로그인 성공 후 URL 확인 */
  async getUrl(): Promise<string> {
    return this.page.url();
  }
}

➡️ LoginPage 클래스는 로그인 페이지에서 입력 필드와 버튼을 다루며, login() 메서드를 통해 로그인 기능을 캡슐화합니다.

3.2. 대시보드 페이지 객체 (pages/dashboardPage.ts)

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

export class DashboardPage {
  private page: Page;
  private profileMenu = '#profile-menu';
  private logoutButton = '#logout';

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

  /** 프로필 메뉴 클릭 */
  async openProfileMenu() {
    await this.page.click(this.profileMenu);
  }

  /** 로그아웃 */
  async logout() {
    await this.page.click(this.logoutButton);
  }

  /** 대시보드 페이지 URL 확인 */
  async getUrl(): Promise<string> {
    return this.page.url();
  }
}

➡️ DashboardPage 클래스는 로그인 후 이동하는 대시보드 페이지의 동작을 정의합니다.

4. Page Object를 사용한 테스트 작성

4.1. 로그인 테스트 (tests/login.test.ts)

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

test('User can log in successfully', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('testuser', 'password123');

  await expect(page).toHaveURL('https://example.com/dashboard');
});

➡️ 로그인 테스트는 LoginPage 객체를 활용하여 UI 요소를 직접 다루지 않고, login() 메서드를 호출하여 간결한 테스트를 작성할 수 있습니다.

4.2. 로그아웃 테스트 (tests/dashboard.test.ts)

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

test('User can log out successfully', async ({ page }) => {
  const loginPage = new LoginPage(page);
  const dashboardPage = new DashboardPage(page);

  await loginPage.goto();
  await loginPage.login('testuser', 'password123');
  await expect(page).toHaveURL('https://example.com/dashboard');

  await dashboardPage.openProfileMenu();
  await dashboardPage.logout();
  await expect(page).toHaveURL('https://example.com/login');
});

➡️ 로그인 후 대시보드에서 로그아웃하는 흐름을 DashboardPage 객체를 활용하여 테스트했습니다.

5. 공통 유틸리티 모듈화 (utils/helpers.ts)

자주 사용되는 기능은 별도 유틸리티 모듈로 관리하면 유지보수성이 향상됩니다.

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

export async function takeScreenshot(page: Page, filename: string) {
  await page.screenshot({ path: `screenshots/${filename}.png` });
}

위 함수를 활용하면 테스트 중 특정 화면을 캡처할 수 있습니다.

6. Playwright 설정 (playwright.config.ts)

Playwright의 기본 설정을 playwright.config.ts 파일에서 정의하여 테스트 환경을 관리합니다.

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

export default defineConfig({
  use: {
    headless: false, // 브라우저 UI 표시 여부
    viewport: { width: 1280, height: 720 }, // 화면 크기
    trace: 'on', // 트레이스 활성화
    screenshot: 'only-on-failure', // 실패 시 스크린샷 저장
  },
});

7. 테스트 실행 방법

Playwright를 실행하여 테스트를 수행하려면 다음 명령어를 입력합니다.

# Playwright 설치
npm install @playwright/test typescript ts-node -D

# Playwright 브라우저 설치
npx playwright install

# 테스트 실행
npx playwright test

개별 테스트 실행

npx playwright test tests/login.test.ts

Playwright에서 Page Object Model(POM)을 적용하면 다음과 같은 이점을 얻을 수 있습니다.

코드 재사용성LoginPageDashboardPage를 만들어 여러 테스트에서 재사용 가능
유지보수 용이성 → UI 요소가 변경되면 하나의 파일만 수정하면 됨
가독성 향상 → 테스트 코드가 간결해지고 로직이 명확해짐
확장성 → 새로운 페이지가 추가될 때 쉽게 확장 가능

이제 POM을 활용하여 체계적으로 Playwright 테스트를 작성할 수 있습니다. 🚀


테스트 케이스 관리 전략

테스트 케이스 관리는 소프트웨어 품질을 유지하면서 개발 프로세스를 원활하게 진행하는 핵심 요소입니다. 특히, Playwright와 같은 E2E 테스트 자동화를 고려할 경우, 테스트 케이스를 체계적으로 관리하면 중복 방지, 유지보수 용이성, 테스트 효율성을 확보할 수 있습니다.

1. 테스트 케이스 관리의 주요 원칙

  1. 명확한 기준 설정
    • 기능 테스트, UI 테스트, 회귀 테스트, 성능 테스트 등 카테고리별로 분류
    • 테스트 목적, 입력 데이터, 예상 결과 명확화
  2. 재사용성 높은 테스트 설계
    • Page Object Model(POM)을 활용하여 UI 요소 및 공통 기능을 모듈화
    • 데이터 기반 테스트(Data-Driven Testing) 적용
  3. 우선순위 기반 테스트 전략
    • 핵심 기능 테스트는 자주 실행 (Smoke Test)
    • 주요 기능 변경 시 수행할 회귀 테스트(Regressive Testing) 별도 관리
    • 비즈니스 크리티컬한 기능과 일반 기능을 구분하여 실행 빈도 조절
  4. 자동화와 수동 테스트의 균형
    • 반복적인 작업은 자동화(E2E, UI, API 테스트)
    • UX, 비주얼, 복잡한 시나리오는 수동 테스트 유지
  5. 효율적인 테스트 실행 및 보고
    • CI/CD에 통합하여 지속적인 테스트 실행
    • 실행 로그, 리포트 자동 생성 및 이슈 트래킹 연계

2. 테스트 케이스 분류 및 문서화

테스트 케이스를 효과적으로 관리하려면 분류 체계 및 문서화가 필요합니다.

2.1. 테스트 유형별 분류

테스트 유형 설명 자동화 여부 실행 빈도
Smoke Test 핵심 기능이 동작하는지 확인 ✅ 자동화 배포마다
기능 테스트 UI/비즈니스 로직 검증 ✅ 자동화 스프린트마다
회귀 테스트 기존 기능이 정상 동작하는지 확인 ✅ 자동화 주요 변경 시
UI 테스트 시각적 요소와 UI 동작 검증 ⛔ 수동 테스트 필요 시
성능 테스트 응답 속도, 부하 테스트 ✅ 자동화 대규모 변경 시
보안 테스트 인증, 권한, 침입 테스트 ⛔ 수동 테스트 필요 시

2.2. 테스트 케이스 문서화

테스트 케이스를 일관성 있게 문서화하면 테스트의 품질과 유지보수성이 향상됩니다.

예제: 테스트 케이스 문서 (Test Case Template)
ID 테스트 이름 테스트 유형 사전 조건 테스트 단계 예상 결과 상태
TC-001 로그인 성공 기능 테스트 회원 계정 존재 1. 로그인 페이지 접근
2. 아이디/비밀번호 입력 후 로그인 클릭
대시보드 이동 Pass
TC-002 로그인 실패 기능 테스트 잘못된 계정 정보 사용 1. 로그인 페이지 접근
2. 틀린 비밀번호 입력 후 로그인 클릭
오류 메시지 출력 Fail

테스트 케이스는 Google Sheets, Notion, TestRail, Jira, Zephyr 등을 활용하여 관리할 수 있습니다.

3. 테스트 케이스 작성 전략

3.1. 핵심 기능을 우선순위로 설정

모든 기능을 동일한 중요도로 테스트할 필요는 없습니다.
비즈니스 로직의 중요도를 기준으로 테스트 우선순위를 설정합니다.

  • P0 (Critical): 로그인, 결제, 계정 생성 등 → 배포 전 항상 테스트
  • P1 (High): 사용자 설정 변경, 데이터 입력 → 주기적으로 테스트
  • P2 (Medium): UI 구성 요소, 비주얼 요소 → 필요 시 테스트

3.2. 데이터 기반 테스트 (Data-Driven Testing)

테스트 케이스를 관리할 때, 다양한 입력 값을 검증해야 하는 경우가 많습니다.
이럴 때 데이터 기반 테스트를 활용하면 중복 없이 효과적으로 관리할 수 있습니다.

예제: Playwright + TypeScript 데이터 기반 테스트
import { test, expect } from '@playwright/test';

const testCases = [
  { username: "testuser", password: "password123", expectedUrl: "https://example.com/dashboard" },
  { username: "wronguser", password: "wrongpass", expectedUrl: "https://example.com/login" }
];

testCases.forEach(({ username, password, expectedUrl }) => {
  test(`Login test for ${username}`, async ({ page }) => {
    await page.goto("https://example.com/login");
    await page.fill("#username", username);
    await page.fill("#password", password);
    await page.click("#login");
    await expect(page).toHaveURL(expectedUrl);
  });
});

장점: 다양한 계정 정보를 한 번에 테스트할 수 있어 코드 중복이 줄어듦

4. 테스트 실행 자동화 및 관리

4.1. CI/CD와 연동하여 지속적인 테스트 실행

  • GitHub Actions / Jenkins / GitLab CI와 같은 CI/CD 도구를 활용하여 테스트 자동 실행
  • 코드 푸시 시 자동 테스트 → 회귀 테스트 자동화
GitHub Actions 예제 (Playwright 테스트 실행)
name: Playwright Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies
        run: npm install
      - name: Run Playwright tests
        run: npx playwright test

이점: 배포 전 자동으로 테스트가 실행되므로 버그 발견 가능

4.2. 테스트 리포트 자동 생성

테스트 실행 후 결과를 쉽게 확인할 수 있도록 리포트를 자동으로 생성합니다.

Playwright 리포트 기능 활성화

playwright.config.ts 파일 수정

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

export default defineConfig({
  use: {
    trace: 'on',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  reporter: [['html', { outputFolder: 'playwright-report' }]],
});

테스트 실행 후 playwright-report/index.html 파일을 열어 결과를 확인할 수 있습니다.

테스트 유형 분류 → Smoke, 기능, 회귀, UI, 성능 테스트를 구분
POM 적용 → 재사용성을 높이고 유지보수를 쉽게
데이터 기반 테스트 활용 → 중복 테스트 코드 제거
우선순위 기반 테스트 관리 → 핵심 기능을 자주 실행
CI/CD 연동 → 배포 전 자동 테스트 실행
리포트 및 이슈 트래킹 → 실행 결과를 시각적으로 확인하고 이슈 추적

이러한 전략을 적용하면 효율적으로 테스트 케이스를 관리하면서 품질을 보장하는 자동화 테스트 환경을 구축할 수 있습니다. 🚀


Playwright에서 테스트 케이스 관리 전략

Playwright를 활용한 테스트 자동화를 효과적으로 운영하려면 테스트 케이스를 체계적으로 관리하는 전략이 필요합니다.
잘못된 테스트 관리 방식은 중복 코드 증가, 유지보수 어려움, 테스트 실행 시간 증가 등의 문제를 초래할 수 있습니다.
따라서 Page Object Model(POM), 데이터 기반 테스트(Data-Driven Testing), CI/CD 연동, 테스트 우선순위 지정 등의 전략을 적용하면 효율적이고 확장 가능한 자동화 테스트 환경을 구축할 수 있습니다.

1. 테스트 케이스 분류 및 관리 체계

테스트 케이스를 명확하게 정의하고, 실행 전략을 세우는 것이 중요합니다.

1.1. 테스트 유형별 분류

테스트 유형 설명 자동화 여부 실행 빈도
Smoke Test 주요 기능이 정상 동작하는지 확인 ✅ 자동화 매 빌드마다
기능 테스트 UI 요소, API, 비즈니스 로직 검증 ✅ 자동화 스프린트마다
회귀 테스트 기존 기능이 변경 없이 유지되는지 검증 ✅ 자동화 주요 업데이트 시
UI 테스트 UI 요소가 정상적으로 표시되는지 확인 ⛔ 수동 테스트 필요 시
성능 테스트 응답 속도, 부하 테스트 ✅ 자동화 대규모 변경 시
보안 테스트 인증, 권한 체크, 침입 테스트 ⛔ 수동 테스트 필요 시

📌 자동화할 테스트 유형
Playwright는 주로 E2E(End-to-End) 테스트, UI 테스트, API 테스트에 적합하므로, Smoke Test, 기능 테스트, 회귀 테스트를 자동화하는 것이 좋습니다.

2. Playwright 테스트 케이스 설계

2.1. Page Object Model(POM) 적용

테스트 케이스의 재사용성과 유지보수성을 높이기 위해 POM (Page Object Model)을 적용합니다.

🔹 로그인 페이지 객체 (pages/loginPage.ts)
import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('https://example.com/login');
  }

  async login(username: string, password: string) {
    await this.page.fill('#username', username);
    await this.page.fill('#password', password);
    await this.page.click('#login');
  }

  async isLoginSuccessful(): Promise<boolean> {
    return await this.page.url() === 'https://example.com/dashboard';
  }
}
🔹 로그인 테스트 (tests/login.test.ts)
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';

test('로그인 성공 테스트', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('testuser', 'password123');
  expect(await loginPage.isLoginSuccessful()).toBeTruthy();
});

장점

  • UI 요소를 LoginPage 객체에 정의하여 재사용성 증가
  • login() 메서드를 활용해 여러 테스트에서 중복 코드 제거

2.2. 데이터 기반 테스트 (Data-Driven Testing)

다양한 입력 값을 활용하여 테스트를 수행하려면 데이터 기반 테스트를 적용하면 효율적입니다.

🔹 데이터 기반 로그인 테스트 (tests/loginData.test.ts)
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';

const testCases = [
  { username: 'validUser', password: 'password123', expected: true },
  { username: 'wrongUser', password: 'wrongPass', expected: false },
];

testCases.forEach(({ username, password, expected }) => {
  test(`Login test - ${username}`, async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login(username, password);
    expect(await loginPage.isLoginSuccessful()).toBe(expected);
  });
});

장점

  • 여러 테스트 데이터를 활용하여 중복 코드 제거
  • 다양한 입력값으로 대량 테스트 가능

2.3. 공통 기능 유틸리티화

자주 사용되는 기능은 유틸리티 모듈로 분리하면 유지보수성이 향상됩니다.

🔹 공통 유틸리티 (utils/helpers.ts)
import { Page } from '@playwright/test';

export async function takeScreenshot(page: Page, filename: string) {
  await page.screenshot({ path: `screenshots/${filename}.png` });
}
🔹 스크린샷 테스트 (tests/screenshot.test.ts)
import { test } from '@playwright/test';
import { takeScreenshot } from '../utils/helpers';

test('홈페이지 스크린샷 저장', async ({ page }) => {
  await page.goto('https://example.com');
  await takeScreenshot(page, 'homepage');
});

장점

  • 유틸리티 모듈을 활용하여 공통 기능 관리 가능
  • 테스트 코드가 간결해지고 가독성이 증가

3. CI/CD 연동 및 테스트 실행 자동화

테스트를 GitHub Actions / Jenkins / GitLab CI와 같은 CI/CD 환경에 연동하면 배포 전 자동으로 실행할 수 있습니다.

3.1. Playwright 설정 (playwright.config.ts)

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

export default defineConfig({
  use: {
    headless: true, // CI 환경에서 브라우저 UI 없이 실행
    viewport: { width: 1280, height: 720 },
    trace: 'on', // 트레이스 활성화
    screenshot: 'only-on-failure', // 실패 시 스크린샷 저장
  },
  reporter: [['html', { outputFolder: 'playwright-report' }]],
});

3.2. GitHub Actions에서 Playwright 실행 (.github/workflows/playwright.yml)

name: Playwright Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies
        run: npm install
      - name: Run Playwright tests
        run: npx playwright test

장점

  • 코드가 변경될 때마다 자동으로 테스트 실행 가능
  • CI 환경에서 실패한 테스트를 추적할 수 있음

4. 테스트 실행 전략

4.1. 테스트 우선순위 지정

테스트 케이스의 실행 빈도를 조절하여 효율적인 테스트 수행이 가능합니다.

우선순위 설명 실행 전략
P0 (Critical) 로그인, 결제, 회원가입 매 빌드마다 실행
P1 (High) 설정 변경, 데이터 입력 주 1회 실행
P2 (Medium) UI 구성 요소, 비주얼 체크 필요 시 실행

📌 Smoke Test, 기능 테스트는 자주 실행하고, UI 테스트는 필요할 때만 실행하는 전략이 효과적입니다.

POM 적용 → UI 요소 및 공통 기능을 모듈화하여 유지보수성을 높임
데이터 기반 테스트 활용 → 다양한 입력값을 효율적으로 테스트
CI/CD 연동 → 배포 전 자동으로 테스트 실행
우선순위 기반 테스트 실행 → 중요 기능 위주로 자주 테스트 수행
리포트 자동 생성playwright-report를 통해 실행 결과 시각화

위 전략을 적용하면 확장 가능하고 유지보수하기 쉬운 Playwright 자동화 테스트 환경을 구축할 수 있습니다. 🚀


공통 동작 및 유틸리티 함수 정의

Playwright 자동화 테스트에서 공통적으로 사용되는 동작과 유틸리티 함수를 분리하여 관리하면 코드의 재사용성유지보수성을 높일 수 있습니다.

이를 위해 Page Object Model(POM)을 적용하여 각 페이지의 공통 동작을 정의하고, 유틸리티 함수를 별도 파일에서 관리하여 테스트 코드에서 쉽게 활용할 수 있도록 합니다.

1. 공통 동작 관리 (BasePage 활용)

모든 페이지에서 공통적으로 사용될 동작(예: goto(), waitForElement(), takeScreenshot())을 BasePage 클래스로 정의하면 각 페이지 객체에서 중복 코드 작성을 피할 수 있습니다.

🔹 pages/basePage.ts (공통 동작 정의)

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

export class BasePage {
  protected page: Page;

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

  /** 특정 URL로 이동 */
  async goto(url: string) {
    await this.page.goto(url);
  }

  /** 특정 요소가 표시될 때까지 대기 */
  async waitForElement(selector: string, timeout: number = 5000) {
    await this.page.waitForSelector(selector, { timeout });
  }

  /** 특정 요소의 텍스트 가져오기 */
  async getText(selector: string): Promise<string> {
    return await this.page.textContent(selector) || '';
  }

  /** 특정 요소가 존재하는지 확인 */
  async isElementVisible(selector: string): Promise<boolean> {
    return await this.page.isVisible(selector);
  }

  /** 스크린샷 캡처 */
  async takeScreenshot(filename: string) {
    await this.page.screenshot({ path: `screenshots/${filename}.png` });
  }

  /** 현재 URL 가져오기 */
  async getCurrentUrl(): Promise<string> {
    return this.page.url();
  }

  /** 특정 버튼 클릭 */
  async clickButton(selector: string) {
    await this.page.click(selector);
  }
}

장점

  • 모든 페이지에서 공통적으로 사용하는 기능을 BasePage에 정의하여 코드 중복 최소화
  • LoginPage, DashboardPage 등 개별 페이지에서 공통 동작을 쉽게 확장 가능

2. 공통 유틸리티 함수 관리

테스트 실행 중 자주 사용되는 스크린샷 캡처, 데이터 포맷 변환, API 호출 등을 별도의 유틸리티 파일에서 관리하면 유지보수가 쉬워집니다.

🔹 utils/helpers.ts (공통 유틸리티 함수 정의)

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

/** 스크린샷 저장 */
export async function takeScreenshot(page: Page, filename: string) {
  await page.screenshot({ path: `screenshots/${filename}.png` });
}

/** 난수 문자열 생성 */
export function generateRandomString(length: number): string {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}

/** 특정 시간 대기 */
export async function delay(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/** API 요청 함수 (GET) */
export async function fetchData(url: string): Promise<any> {
  const response = await fetch(url);
  return response.json();
}

장점

  • takeScreenshot()를 활용하여 실패한 테스트의 스크린샷 자동 저장 가능
  • generateRandomString()으로 동적 데이터 입력을 위한 난수 문자열 생성 가능
  • fetchData()를 통해 Playwright 테스트에서 API 데이터를 가져와 활용 가능

3. Page Object Model (POM) 활용하여 공통 동작 적용

위에서 정의한 BasePage를 활용하여 각 페이지의 동작을 정의하면 더욱 효율적인 테스트 코드 작성을 할 수 있습니다.

🔹 pages/loginPage.ts (로그인 페이지 객체)

import { BasePage } from './basePage';

export class LoginPage extends BasePage {
  private usernameInput = '#username';
  private passwordInput = '#password';
  private loginButton = '#login';

  /** 로그인 페이지 이동 */
  async goto() {
    await super.goto('https://example.com/login');
  }

  /** 로그인 수행 */
  async login(username: string, password: string) {
    await this.page.fill(this.usernameInput, username);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.loginButton);
  }

  /** 로그인 성공 여부 확인 */
  async isLoginSuccessful(): Promise<boolean> {
    return await super.isElementVisible('#dashboard');
  }
}

장점

  • BasePage를 확장하여 LoginPage에서 공통 기능을 재사용 가능
  • goto(), isElementVisible() 등의 메서드를 상속받아 중복 코드 제거

4. 공통 동작 및 유틸리티 적용한 테스트 코드 작성

🔹 tests/login.test.ts

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';
import { takeScreenshot, generateRandomString } from '../utils/helpers';

test('사용자가 로그인할 수 있어야 한다', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();

  const randomUsername = generateRandomString(8);
  await loginPage.login(randomUsername, 'password123');

  if (!(await loginPage.isLoginSuccessful())) {
    await takeScreenshot(page, 'login-failed');
  }

  expect(await loginPage.isLoginSuccessful()).toBeTruthy();
});

적용된 공통 기능

  • generateRandomString(8)난수 문자열을 활용하여 유효한 사용자 ID 테스트
  • takeScreenshot(page, 'login-failed')로그인 실패 시 스크린샷 자동 저장
  • LoginPageisLoginSuccessful()을 호출하여 로그인 성공 여부 확인

5. CI/CD에서 공통 동작 활용

Playwright 테스트는 CI/CD 환경에서 실행될 때 자동으로 공통 동작과 유틸리티를 활용하여 안정성을 높일 수 있습니다.

🔹 playwright.config.ts (테스트 설정)

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

export default defineConfig({
  use: {
    headless: true,
    viewport: { width: 1280, height: 720 },
    trace: 'on',
    screenshot: 'only-on-failure',
  },
  reporter: [['html', { outputFolder: 'playwright-report' }]],
});

🔹 GitHub Actions에서 실행 (.github/workflows/playwright.yml)

name: Playwright Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies
        run: npm install
      - name: Run Playwright tests
        run: npx playwright test

장점

  • 테스트 실행 시 공통 동작이 자동 적용됨
  • CI 환경에서도 takeScreenshot()을 활용하여 실패한 테스트 분석 가능

BasePage 활용goto(), waitForElement() 등 공통 동작을 상속하여 재사용
유틸리티 함수 모듈화takeScreenshot(), generateRandomString() 등 별도 관리
POM 적용LoginPage, DashboardPage 등 각 페이지별 객체 정의
CI/CD 연동 → GitHub Actions 등과 연계하여 자동화 실행

위와 같은 전략을 적용하면 유지보수가 쉬운 확장 가능한 Playwright 자동화 테스트 환경을 구축할 수 있습니다. 🚀


공통 동작을 쉽게 확장 가능하다는 의미

BasePage 클래스에서 정의된 공통 동작은 Playwright에서 테스트 시 반복적으로 사용되는 기본 동작을 캡슐화합니다. 이를 기반으로 LoginPage, DashboardPage와 같은 개별 페이지 클래스에서 중복 없이 기본 동작을 상속받아 확장할 수 있습니다. 이는 코드 재사용성을 높이고, 새로운 페이지를 추가하거나 기존 페이지를 수정할 때 최소한의 작업만으로 동작을 확장할 수 있도록 도와줍니다.

1. 공통 동작의 정의

BasePage 클래스는 모든 페이지에서 공통적으로 수행할 수 있는 동작들을 정의합니다. 예를 들어, 아래와 같은 동작이 자주 사용될 수 있습니다.

  • 특정 URL로 이동 (goto)
  • 특정 요소의 가시성 확인 (isElementVisible)
  • 버튼 클릭 (clickButton)
  • 텍스트 가져오기 (getText)
  • 스크린샷 저장 (takeScreenshot)

BasePage 코드 예제

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

export class BasePage {
  protected page: Page;

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

  async goto(url: string) {
    await this.page.goto(url);
  }

  async isElementVisible(selector: string): Promise<boolean> {
    return await this.page.isVisible(selector);
  }

  async clickButton(selector: string) {
    await this.page.click(selector);
  }

  async getText(selector: string): Promise<string> {
    return (await this.page.textContent(selector)) || '';
  }

  async takeScreenshot(filename: string) {
    await this.page.screenshot({ path: `screenshots/${filename}.png` });
  }
}

핵심

  • 공통적으로 사용되는 동작을 하나의 클래스에 캡슐화
  • 상속을 통해 중복 제거 및 코드 간소화

2. 개별 페이지에서 공통 동작 확장

BasePage를 상속받아 LoginPageDashboardPage와 같은 개별 페이지를 정의합니다.
이때, 각 페이지의 고유 동작만 추가하면 되므로 코드 중복이 최소화됩니다.

LoginPage에서 확장된 동작

import { BasePage } from './basePage';

export class LoginPage extends BasePage {
  private usernameInput = '#username';
  private passwordInput = '#password';
  private loginButton = '#login';

  /** 로그인 페이지로 이동 */
  async goto() {
    await super.goto('https://example.com/login');
  }

  /** 사용자 로그인 */
  async login(username: string, password: string) {
    await this.page.fill(this.usernameInput, username);
    await this.page.fill(this.passwordInput, password);
    await super.clickButton(this.loginButton); // 공통 동작 재사용
  }

  /** 로그인 성공 여부 확인 */
  async isLoginSuccessful(): Promise<boolean> {
    return await super.isElementVisible('#dashboard'); // 공통 동작 재사용
  }
}

DashboardPage에서 확장된 동작

import { BasePage } from './basePage';

export class DashboardPage extends BasePage {
  private profileMenu = '#profile-menu';
  private logoutButton = '#logout';

  /** 프로필 메뉴 열기 */
  async openProfileMenu() {
    await super.clickButton(this.profileMenu); // 공통 동작 재사용
  }

  /** 로그아웃 */
  async logout() {
    await super.clickButton(this.logoutButton); // 공통 동작 재사용
  }

  /** 대시보드 URL 확인 */
  async isDashboardVisible(): Promise<boolean> {
    return await super.isElementVisible('#dashboard-content'); // 공통 동작 재사용
  }
}

공통 동작 확장의 핵심

  • BasePage에서 정의한 메서드(goto, clickButton, isElementVisible)를 재사용하여 중복 없이 간결한 코드 작성.
  • 개별 페이지는 페이지 고유의 요소(예: 로그인 필드, 로그아웃 버튼 등)를 정의하여 필요한 동작만 추가.

3. 공통 동작 확장의 이점

3.1. 코드 중복 감소

BasePage를 상속받으면 공통적인 기능을 재정의할 필요가 없으므로, 코드의 양이 크게 줄어듭니다.
예를 들어, LoginPageDashboardPage 모두 URL 이동, 버튼 클릭, 요소 가시성 확인 등의 기본 동작을 사용하지만, 이를 BasePage에서 한 번만 정의하면 됩니다.

3.2. 유지보수 용이

공통 동작을 업데이트해야 할 경우, BasePage 클래스만 수정하면 모든 하위 페이지에 적용됩니다.
예를 들어, 버튼 클릭 동작(clickButton)에 로깅 기능을 추가하려면 BasePage의 해당 메서드만 수정하면 됩니다.

async clickButton(selector: string) {
  console.log(`Clicking button: ${selector}`);
  await this.page.click(selector);
}

3.3. 확장성

새로운 페이지를 추가해야 할 경우, BasePage를 상속받아 고유 동작만 정의하면 됩니다.
예를 들어, 결제 페이지(PaymentPage)를 추가할 때도 공통 동작은 그대로 활용 가능하며, 결제 관련 메서드만 새로 정의하면 됩니다.

export class PaymentPage extends BasePage {
  private cardNumberInput = '#card-number';
  private payButton = '#pay';

  async enterCardDetails(cardNumber: string) {
    await this.page.fill(this.cardNumberInput, cardNumber);
  }

  async pay() {
    await super.clickButton(this.payButton); // 공통 동작 재사용
  }
}

4. 테스트 코드에서의 활용

BasePage를 상속받은 페이지 객체를 활용하여 테스트 코드를 간결하고 재사용성 높게 작성할 수 있습니다.

예제: 로그인 테스트

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

test('사용자가 로그인할 수 있어야 한다', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto(); // BasePage의 공통 동작 활용
  await loginPage.login('testuser', 'password123'); // LoginPage 고유 동작
  expect(await loginPage.isLoginSuccessful()).toBeTruthy(); // BasePage 확장
});

예제: 로그아웃 테스트

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

test('사용자가 로그아웃할 수 있어야 한다', async ({ page }) => {
  const loginPage = new LoginPage(page);
  const dashboardPage = new DashboardPage(page);

  await loginPage.goto();
  await loginPage.login('testuser', 'password123');
  expect(await loginPage.isLoginSuccessful()).toBeTruthy();

  await dashboardPage.openProfileMenu();
  await dashboardPage.logout();
  expect(await page.url()).toBe('https://example.com/login');
});

BasePage를 활용하여 공통 동작을 정의하고, 개별 페이지 클래스에서 이를 확장하면 다음과 같은 장점을 얻을 수 있습니다.

  1. 코드 중복 최소화: 공통 동작은 한 곳에서 관리하며 모든 페이지에서 재사용 가능.
  2. 유지보수 용이성: BasePage를 수정하면 하위 페이지에도 변경 사항 자동 적용.
  3. 확장성: 새로운 페이지를 추가할 때 고유한 동작만 정의하면 쉽게 확장 가능.
  4. 테스트 코드 간결화: 페이지 객체를 활용하여 테스트 로직이 명확하고 직관적.

이를 통해 Playwright 테스트의 재사용성, 유지보수성, 확장성을 모두 확보할 수 있습니다. 🚀