어서션

어서션 (Assertions)

테스트가 기대하는 상태를 검증하는 일, 그게 바로 어서션이에요. Playwright의 어서션은 단순히 값을 비교하는 것에 그치지 않고, 사이트가 비동기로 변하는 상태까지 자동으로 기다려 준다는 특징이 있어요. 어떤 어서션이 있고, 언제 어떤 걸 골라야 하는지 정리해 볼게요.

출처: Playwright 공식 문서 — Assertions

본문

expect 함수

Playwright는 expect 함수 형태로 테스트 어서션을 제공해요. 어서션을 하려면 expect(value)를 호출하고 기대를 나타내는 매처(matcher)를 고르면 돼요. toEqual, toContain, toBeTruthy 같은 일반 매처가 많아서 어떤 조건이든 어서션할 수 있어요.

expect(success).toBeTruthy();

Playwright에는 웹 전용 비동기 매처도 있어요. 이것들은 기대 조건이 충족될 때까지 기다려요. 다음 예시를 볼게요.

await expect(page.getByTestId('status')).toHaveText('Submitted');

테스트 id가 status인 요소가 "Submitted" 텍스트를 가질 때까지 Playwright는 요소를 다시 가져와 반복해서 확인해요. 조건이 충족되거나 타임아웃에 도달할 때까지요. 이 타임아웃은 직접 넘기거나 테스트 설정의 [property: TestConfig.expect] 값으로 한 번에 설정할 수 있어요. 기본 어서션 타임아웃은 5초예요.

자동 재시도 어서션

다음 어서션들은 통과하거나 어서션 타임아웃에 도달할 때까지 재시도해요. 재시도 어서션은 비동기이므로 반드시 await해야 한다는 점을 기억하세요.

Assertion Description
await expect(locator).toBeAttached() 요소가 붙어 있음
await expect(locator).toBeChecked() 체크박스가 체크됨
await expect(locator).toBeDisabled() 요소가 비활성
await expect(locator).toBeEditable() 요소가 편집 가능
await expect(locator).toBeEmpty() 컨테이너가 비어 있음
await expect(locator).toBeEnabled() 요소가 활성
await expect(locator).toBeFocused() 요소가 포커스됨
await expect(locator).toBeHidden() 요소가 보이지 않음
await expect(locator).toBeInViewport() 요소가 뷰포트와 교차
await expect(locator).toBeVisible() 요소가 보임
await expect(locator).toContainText() 요소가 텍스트를 포함
await expect(locator).toContainClass() 요소가 지정 CSS 클래스를 가짐
await expect(locator).toHaveAttribute() 요소가 DOM 속성을 가짐
await expect(locator).toHaveClass() 요소가 지정 CSS 클래스 속성을 가짐
await expect(locator).toHaveCount() 리스트가 정확한 자식 수를 가짐
await expect(locator).toHaveCSS() 요소가 CSS 속성을 가짐
await expect(locator).toHaveId() 요소가 ID를 가짐
await expect(locator).toHaveRole() 요소가 특정 ARIA role을 가짐
await expect(locator).toHaveText() 요소가 텍스트와 일치
await expect(locator).toHaveValue() input이 값을 가짐
await expect(page).toHaveTitle() 페이지가 제목을 가짐
await expect(page).toHaveURL() 페이지가 URL을 가짐
await expect(response).toBeOK() 응답이 OK 상태

재시도하지 않는 어서션

다음 어서션들은 어떤 조건이든 테스트할 수 있지만 자동 재시도하지 않아요. 대부분의 웹 페이지는 정보를 비동기로 보여주기 때문에 재시도하지 않는 어서션은 불안정한(flaky) 테스트를 만들기 쉬워요. 가능하면 자동 재시도 어서션을 선호하세요. 더 복잡한 조건이면 expect.poll이나 expect.toPass를 쓰면 됩니다.

Assertion Description
[method: GenericAssertions.toBe] 값이 같음
[method: GenericAssertions.toBeCloseTo] 숫자가 대략적으로 같음
[method: GenericAssertions.toBeDefined] 값이 undefined가 아님
[method: GenericAssertions.toBeFalsy] 값이 falsy(false, 0, null 등)
[method: GenericAssertions.toBeGreaterThan] 숫자가 더 큼
[method: GenericAssertions.toBeGreaterThanOrEqual] 숫자가 더 크거나 같음
[method: GenericAssertions.toBeInstanceOf] 객체가 클래스의 인스턴스
[method: GenericAssertions.toBeLessThan] 숫자가 더 작음
[method: GenericAssertions.toBeLessThanOrEqual] 숫자가 더 작거나 같음
[method: GenericAssertions.toBeNaN] 값이 NaN
[method: GenericAssertions.toBeNull] 값이 null
[method: GenericAssertions.toBeTruthy] 값이 truthy
[method: GenericAssertions.toBeUndefined] 값이 undefined
[method: GenericAssertions.toContain#1] 문자열이 부분 문자열 포함
[method: GenericAssertions.toContain#2] 배열·셋이 요소 포함
[method: GenericAssertions.toContainEqual] 배열·셋이 유사한 요소 포함
[method: GenericAssertions.toEqual] 값이 유사함(깊은 동등·패턴 매칭)
[method: GenericAssertions.toHaveLength] 배열·문자열이 길이를 가짐
[method: GenericAssertions.toHaveProperty] 객체가 속성을 가짐
[method: GenericAssertions.toMatch] 문자열이 정규식과 일치
[method: GenericAssertions.toMatchObject] 객체가 지정 속성을 포함
[method: GenericAssertions.toStrictEqual] 값이 유사함(속성 타입 포함)
[method: GenericAssertions.toThrow] 함수가 오류를 던짐

비대칭 매처 (Asymmetric matchers)

이 표현들은 다른 어서션 안에 중첩해 더 유연한 매칭을 가능하게 해요.

Matcher Description
expect.any() 클래스·원시 타입의 어떤 인스턴스든 매칭
expect.anything() 무엇이든 매칭
expect.arrayContaining() 배열이 특정 요소 포함
expect.arrayOf() 배열이 특정 타입의 요소 포함
expect.closeTo() 숫자가 대략적으로 같음
expect.objectContaining() 객체가 특정 속성 포함
expect.stringContaining() 문자열이 부분 문자열 포함
expect.stringMatching() 문자열이 정규식과 일치

부정 매처

매처 앞에 .not을 붙이면 반대를 기대할 수 있어요.

expect(value).not.toEqual(0);
await expect(locator).not.toContainText('some text');

소프트 어서션 (Soft assertions)

기본적으로 실패한 어서션은 테스트 실행을 종료해요. Playwright는 소프트 어서션도 지원하는데, 실패해도 테스트를 종료하지 않고 테스트를 실패로 표시만 해요.

// Make a few checks that will not stop the test when failed...
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');

// ... and continue the test to check more things.
await page.getByRole('link', { name: 'next page' }).click();
await expect.soft(page.getByRole('heading', { name: 'Make another order' })).toBeVisible();

테스트 중 아무 때나 소프트 어서션 실패가 있었는지 확인할 수도 있어요.

await expect.soft(page.getByTestId('status')).toHaveText('Success');
await expect.soft(page.getByTestId('eta')).toHaveText('1 day');

// Avoid running further if there were soft assertion failures.
expect(test.info().errors).toHaveLength(0);

소프트 어서션은 Playwright 테스트 러너에서만 동작해요.

커스텀 expect 메시지

expect 함수의 두 번째 인자로 커스텀 메시지를 지정할 수 있어요.

await expect(page.getByText('Name'), 'should be logged in').toBeVisible();

이 메시지는 통과·실패 어서션 모두 리포터에 표시되어 어서션에 대한 더 많은 맥락을 제공해요. 성공하면 이런 스텝이 보이고,

✅ should be logged in    @example.spec.ts:18

실패하면 오류가 이렇게 보여요.

    Error: should be logged in

    Call log:
      - expect.toBeVisible with timeout 5000ms
      - waiting for "getByText('Name')"

소프트 어서션도 커스텀 메시지를 지원해요.

expect.soft(value, 'my soft assertion').toBe(56);

expect.configure

timeout, soft 같은 기본값을 따로 가진 미리 구성된 expect 인스턴스를 만들 수 있어요.

const slowExpect = expect.configure({ timeout: 10000 });
await slowExpect(locator).toHaveText('Submit');

// Always do soft assertions.
const softExpect = expect.configure({ soft: true });
await softExpect(locator).toHaveText('Submit');

expect.poll

동기 expect를 비동기 폴링 방식으로 바꿀 수 있어요. 다음은 주어진 함수가 HTTP 200을 반환할 때까지 폴링하는 예시예요.

await expect.poll(async () => {
  const response = await page.request.get('https://api.example.com');
  return response.status();
}, {
  // Custom expect message for reporting, optional.
  message: 'make sure API eventually succeeds',
  // Poll for 10 seconds; defaults to 5 seconds. Pass 0 to disable timeout.
  timeout: 10000,
}).toBe(200);

커스텀 폴링 간격도 지정할 수 있어요.

await expect.poll(async () => {
  const response = await page.request.get('https://api.example.com');
  return response.status();
}, {
  // Probe, wait 1s, probe, wait 2s, probe, wait 10s, probe, wait 10s, probe
  // ... Defaults to [100, 250, 500, 1000].
  intervals: [1_000, 2_000, 10_000],
  timeout: 60_000
}).toBe(200);

expect.softexpect.poll을 조합하면 폴링 로직에서 소프트 어서션을 쓸 수 있어요. expect.configure({ soft: true })expect.poll과 체이닝됩니다.

expect.toPass

통과할 때까지 코드 블록을 재시도할 수 있어요.

await expect(async () => {
  const response = await page.request.get('https://api.example.com');
  expect(response.status()).toBe(200);
}).toPass();

커스텀 타임아웃과 재시도 간격도 지정할 수 있어요. 기본적으로 toPass는 타임아웃이 0이고 커스텀 expect 타임아웃을 따르지 않는다는 점을 알아 두세요.

await expect(async () => {
  const response = await page.request.get('https://api.example.com');
  expect(response.status()).toBe(200);
}).toPass({
  intervals: [1_000, 2_000, 10_000],
  timeout: 60_000
});

expect.extend로 커스텀 매처 추가

커스텀 매처를 제공해 Playwright 어서션을 확장할 수 있어요. 매처는 pass 플래그(통과 여부)와 message 콜백(실패 시 사용)을 반환해야 해요.

import { expect as baseExpect } from '@playwright/test';
import type { Locator } from '@playwright/test';

export { test } from '@playwright/test';

export const expect = baseExpect.extend({
  async toHaveAmount(locator: Locator, expected: number, options?: { timeout?: number }) {
    const assertionName = 'toHaveAmount';
    let pass: boolean;
    let matcherResult: any;
    try {
      const expectation = this.isNot ? baseExpect(locator).not : baseExpect(locator);
      await expectation.toHaveAttribute('data-amount', String(expected), options);
      pass = true;
    } catch (e: any) {
      matcherResult = e.matcherResult;
      pass = false;
    }

    if (this.isNot) {
      pass =!pass;
    }

    const message = pass
      ? () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
          '\n\n' +
          `Locator: ${locator}\n` +
          `Expected: not ${this.utils.printExpected(expected)}\n` +
          (matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
      : () =>  this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
          '\n\n' +
          `Locator: ${locator}\n` +
          `Expected: ${this.utils.printExpected(expected)}\n` +
          (matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '');

    return {
      message,
      pass,
      name: assertionName,
      expected,
      actual: matcherResult?.actual,
    };
  },
});

이제 테스트에서 toHaveAmount를 쓸 수 있어요.

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

test('amount', async () => {
  await expect(page.locator('.cart')).toHaveAmount(4);
});

Playwright의 expectexpect 라이브러리와 혼동하지 마세요. 후자는 Playwright 테스트 러너와 완전히 통합되지 않아요. 여러 파일·모듈의 커스텀 매처는 mergeTestsmergeExpects로 조합할 수 있어요.

import { mergeTests, mergeExpects } from '@playwright/test';
import { test as dbTest, expect as dbExpect } from 'database-test-utils';
import { test as a11yTest, expect as a11yExpect } from 'a11y-test-utils';

export const expect = mergeExpects(dbExpect, a11yExpect);
export const test = mergeTests(dbTest, a11yTest);

더 알아보기