본문 바로가기
자동화(Automation)/Playwright

Playwright_Annotations, Hooks, Fixtures

by Testengineer 2025. 10. 17.
반응형

selenium에서는 TestNG를 통해서 Java의 annotation을 이용했다. 이처럼 playwright에서 사용되는 Annotation 기능을 알아보고자 포스팅을 한다.

굳이 따지자면 TestNG와 playwright의 annotation이 개념적으로는 테스트를 제어한다는 점에서 비슷하지만 차이점이 더 크다. playwright의 annotation은 주로 테스트 상태를 표시하는 TestNG의 일부 기능만을 담당한다. 

playwright에서는 Hooks이 테스트 실행 전후로 특정작업을 수행하는 함수로 사용할 수 있게 하는 방법이다.

 

Annotations

우선 play wright에서는 테스트 상태와 조건을 표현하기 위해 아래와 같은 방법을 사용한다.

  • test.skip() - 테스트 건너뛰기 : 특정환경에서 테스트 할 필요가 없을 때 실행하지 않는 것
  • test.fail() - 실패 예상 테스트 : 실패해야 정상인 케이스
  • test.fixme() - 수정 필요 테스트 : 테스트가 너무 느리거나 크래시가 일어날 때 테스트를 실행하지 않음, fail로 표시
  • test.slow() - 느린 테스트 : 테스트 타임아웃이 3배로 늘려짐

 

annotations에 커스텀을 해서 type과 description을 이용해 해당 설명을 추가 가능하다. 커스텀 내용을 추가시에는, 테스트 추적이나 분류할 때 편리하다.

test('example test', async ({ page, browser }) => {
  test.info().annotations.push({
    type: 'browser version',
    description: browser.version(),
  });

  // ...
});

 

test문에서 조건을 줄 때 skip 하는 경우, 브라우저가 파이어폭스인 경우에 스킵한다

test('skip this test', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'Still working on it');
});

 

테스트 실패했을 경우 스크린샷이나 스크린 녹화가 필요한 경우 아래 예시문처럼 사용할 수 있다.

아래 예시는 테스트 실패 시에만 스크린샷을 찍고, 첫 재 시작 시에만 비디오녹화와 trace를 기록하는 내용이다

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

export default defineConfig({
  use: {
    // Capture screenshot after each test failure.
    screenshot: 'only-on-failure',

    // Record trace only when retrying a test for the first time.
    trace: 'on-first-retry',

    // Record video only when retrying a test for the first time.
    video: 'on-first-retry'
  },
});

 

 

Hooks

hooks는 테스트 실행 전후에 특정 작업을 수행하는 함수들이다.

  • beforeEach : 각 테스트 실행 전에 실행, 페이지이동이나 로그인 등 공통 setup을 주로 한다.
  • afterEach : 각 테스트 실행 후에 실행, 쿠키/캐시 정리 혹은 로그아웃을 주로 한다.
  • beforeAll : 모든 테스트 실행 전에 한 번만 실행, 테스트 데이터를 생성한다.
  • afterAll : 모든 테스트 실행 후에 한 번만 실행, 생성된 파일 삭제등을 진행한다.

실행순서로 보자면 beforeAll > beforeEach > test문 > afterEach > afterAll 순서로 작동한다.

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

test.describe('로그인 테스트 그룹', () => {
  
  // 모든 테스트 시작 전 한 번 실행
  test.beforeAll(async () => {
    console.log('테스트 환경 설정');
  });

  // 각 테스트 전에 실행
  test.beforeEach(async ({ page }) => {
    await page.goto('https://example.com/login');
  });

  // 각 테스트 후에 실행
  test.afterEach(async ({ page, context }) => {
    await context.clearCookies();
  });

  // 모든 테스트 후 한 번 실행
  test.afterAll(async () => {
    console.log('테스트 환경 정리');
  });

  // 일반 테스트
  test('유효한 로그인', async ({ page }) => {
    await page.fill('#username', 'user@example.com');
    await page.fill('#password', 'password123');
    await page.click('#login-button');
    await expect(page).toHaveURL(/dashboard/);
  });

  // 조건부 건너뛰기
  test('소셜 로그인', async ({ page, browserName }) => {
    test.skip(browserName === 'firefox', 'Firefox 미지원');
    await page.click('#social-login');
  });

  // 알려진 버그로 실패 예상
  test.fail('비밀번호 찾기 버그', async ({ page }) => {
    await page.click('#forgot-password');
    await expect(page.locator('.error')).not.toBeVisible();
    // 현재 버그로 에러가 보임 → 실패 예상
  });

  // 느린 테스트
  test.slow('이메일 인증 플로우', async ({ page }) => {
    // 이메일 받는 시간이 오래 걸림
    await page.fill('#email', 'test@example.com');
    await page.click('#send-verification');
    await page.waitForSelector('.verification-sent', { timeout: 60000 });
  });

  // 수정 필요한 테스트
  test.fixme('2FA 인증 - 크래시 발생', async ({ page }) => {
    // 현재 크래시 발생으로 실행 안 함
  });

});

 

 

그리고 추가로 test.describe.configure()을 통해서 병렬/직렬 제어가 가능하다. 디폴트로 playwright는 병렬 실행이다. 하지만 순차적으로 실행할 경우 모드를 'serial'을 사용하면 된다.

const { test, expect } = require('@playwright/test');

// 순차적 실행
test.describe.configure({ mode: 'serial' });

test.describe('순차 실행 그룹', () => {
  test('1번 테스트', async ({ page }) => {
    // 이게 끝나야 다음 실행
  });
  
  test('2번 테스트', async ({ page }) => {
    // 1번 끝난 후 실행
  });
});

여기서 첫 실패 시 중단하려면 test.describe.serial을 사용하면 된다.

const { test, expect } = require('@playwright/test');

test.describe.serial('로그인 플로우', () => {
  test('1단계: 로그인', async ({ page }) => {
    // 실패하면 여기서 멈춤
  });
  
  test('2단계: 프로필 확인', async ({ page }) => {
    // 1단계 성공해야 실행됨
  });
  
  test('3단계: 로그아웃', async ({ page }) => {
    // 1,2단계 성공해야 실행됨
  });
});

configure({mode:'serial'}) 는 실패해도 다음 테스트를 실행하지만 describe.serial()은 실패하면 나머지를 건너뛰는 차이점이 있다.

 

만약 테스트문을 작성하다가 특정 테스트문만 실행하고 싶다면 test.only()나 test.describe.only()을 사용하면 가능하다. 하지만 주의점으로는 only()가 있다면 CI에서 문제가 발생할 수 있기 때문에 개발 중에만 사용하는 것이 좋다.

const { test, expect } = require('@playwright/test');

test.only('이것만 실행', async ({ page }) => {
  // 이 테스트만 실행됨
});

test('건너뜀', async ({ page }) => {
  // 실행 안 됨
});

// 그룹 단위로도 가능
test.describe.only('이 그룹만', () => {
  test('테스트 1', async ({ page }) => {});
  test('테스트 2', async ({ page }) => {});
});

 

Fixtures

그리고 playwright에 기능 중에 특이점으로는 커스텀 fixtures라는 것이 있다. 이건 셋업을 할 때 필요한 기능인데 테스트에 필요한 환경이나 데이터를 설정해서 재사용 가능한 객체를 의미한다.

playwright가 자동으로 제공하는 fixtures로는 page(브라우저 페이지), context(브라우저 컨텍스트), browser(브라우저 인스턴스), browserName(브라우저 이름), request(API 요청객체)가 있다.

Hooks는 파일 내 제한적으로 재사용한다면, Fixtures는 전체 프로젝트에 재사용되기 때문에 훨씬 유연성이 높고 여러 테스트 파일에서 공통 setup이 필요할 때 사용되는 것이 좋다.

// fixtures/auth.js
const { test as base } = require('@playwright/test');

exports.test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Setup: 로그인 수행
    await page.goto('https://example.com/login');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
    
    // 테스트에 로그인된 페이지 전달
    await use(page);
    
    // Teardown: 로그아웃 (선택사항)
    await page.click('#logout');
  },
});
// tests/dashboard.spec.js
const { test } = require('../fixtures/auth');
const { expect } = require('@playwright/test');

test('대시보드 확인', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/dashboard');
  await expect(authenticatedPage.locator('h1')).toHaveText('Dashboard');
});

test('프로필 수정', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/profile');
  await authenticatedPage.fill('#name', 'New Name');
  await authenticatedPage.click('#save');
});
반응형