playwright

playwright

POM (Page Object Model)

Playwright에서 유지보수성, 가독성, 재사용성을 높이는 디자인 패턴

  • 페이지별로 클래스를 만들고 액션을 메서드로 캡슐화

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

export class BaseHelper {
  readonly page: Page;
  readonly context: BrowserContext;
  readonly baseUrl: string;

  constructor(page: Page, context: BrowserContext) {
    this.page = page;
    this.context = context;
    this.baseUrl = process.env.NEXT_PUBLIC_WEB_BASE_URL;
  }

  async downloadImage() {
    await this.page.getByRole('button', { name: 'download' }).click();
    await expect(this.page.getByText('success')).toBeVisible();
  }

  async signIn(authorization: string) {
    await this.context.addCookies([
      {
        name: 'authorization',
        value: authorization,
        url: this.baseUrl,
      },
    ]);
  }
}

ETC

  • evaluate : display: none 인 요소는 playwright 통해서 이벤트가 트리거가 제데로 되지 않는데 그럴 때 사용

 const helper = new Helper(page, context);

  const setFile = helper.uploadFile();
  await helper.getThumbnail.evaluate((el: HTMLInputElement) =>
    el.click()
  );
  await setFile(imgFileName);

  await expect(helper.getThumbnailSrc).not.toHaveAttribute(
    'src',
    '/file.svg'
  );
  • playwright 동작상 딜레이에 따라 동작이 달라질 수 있다.

    • 처음 disabled되고 나중 결과도 disabled 되어야하는 경우에는 동작이 전파되기 전에 playwright이 이미 통과됬다고 판단

    • 가능하다면 항상 변화를 대기했다가 감지하는 방식으로 테스트를 짜자.

async setUpValidation() {
    const title = faker.string.sample(2);
    const fileName = imgFileName;

    await this.fillForm({
      title,
      fileName,
    });
    await expect(this.getSumbit).toBeEnabled();
  }
  • uploadFile

  uploadFile() {
    const fileChooserPromise = this.page.waitForEvent('filechooser');

    const setFile = async (fileName: string) => {
      const fileChooser = await fileChooserPromise;
      await fileChooser.setFiles(path.join('__tests__', 'fixtures', fileName));
    };

    return setFile;
  }
  • strictHaveUrl

  async strictHaveUrl(relative: string) {
    const reg = new RegExp(`^${this.baseUrl}${relative}$`);
    await expect(this.page).toHaveURL(reg);
  }
  • signIn

 async signIn(authorization: string) {
    await this.context.addCookies([
      {
        name: 'authorization',
        value: authorization,
        url: this.baseUrl,
      },
    ]);
  }

꿀팁

셋업이 너무 많아서 테스트하기 어려운 경우에는 굳이 테스트하지 말자. 모든 걸 다 테스트해야 한다는 강박은 버리자.

테스트 블럭이 아닌 테스트 훅을 더 중요시하다보면 관심사 기준이 아닌 훅 기준으로 테스트가 묶이게됨

훅을 거의 안쓰는걸 추천

  • Playwright에서 말하는 훅(Hook)은 테스트 실행 전후에 특정 작업을 수행할 수 있도록 도와주는 setup*설정 & teardown*정리 함수임.

  • 훅을 중심으로 테스트를 짜면, 관심사(테스트 목적) 중심이 아니라 실행 순서 중심으로 묶이게 됨 → 유지보수가 어려워짐.

    • beforeEach 훅이 중심이 되어 테스트가 작성됨

    • 이후 새로운 테스트를 추가할 때 관심사(유저 이름, ID)가 아니라, beforeEach를 공유할 수 있는지 여부로 테스트가 묶이게 됨

    • 만약 user 설정이 달라진다면 훅을 수정해야 하고, 의도치 않은 테스트가 깨질 수도 있음

  • 테스트 블록(test)을 중심으로 테스트를 작성하면 독립성이 보장되고, 수정하기 편함.

    • 관심사별로 독립적인 테스트를 작성할 수 있음

  • DB 초기화, 공통적인 API 모킹 등에는 훅을 사용할 수 있지만, 남용하면 테스트가 서로 의존하게 되므로 주의해야 함.

    • 등등 공통 작업이 필요할 때

    • Playwright에서 공통적인 페이지 이동이 필요할 때

mocking vs fixtures

  • 정적인 응답은 모킹

  • 동적인 응답은 픽스처 -> 요청은 같은데 영속계층이 바뀌어서 응답이 바뀌는 경우

Last updated