본문으로 건너뛰기
중급약 35분

테스트 데이터 부재 해결 — API Fixture 자동화 설계와 실패 단계별 자동 트리아지

E2E 테스트가 "시험할 주문이 없어서", "쿠폰이 이미 쓰여서" 실패한다면 그것은 테스트 데이터 부재 문제다. 사전 조건을 추출해 API로 상태를 만들고 테스트에 주입한 뒤 실행하고 정리하는 다섯 단계 파이프라인, Playwright test.extend 기반 Custom Fixture 구현, UI로 데이터를 만들지 않는 원칙과 fixture_payload 메타데이터·멱등성 접두사·API 토큰 사전 주입, 그리고 Fixture 단계 실패는 환경 문제로, UI 단계 실패는 셀렉터 오류나 실제 버그로 자동 분류하는 트리아지 로직을 다룬다.

  • #테스트데이터
  • #Fixture
  • #Playwright
  • #API자동화
  • #트리아지
  • #E2E

개념

테스트가 실패한 이유가 "데이터가 없어서"일 때

E2E 테스트가 이런 이유로 실패하는 것을 자주 본다.

  • "배송 중인 주문 상세 화면" 테스트 — 스테이징에 배송 중 주문이 하나도 없다
  • "쿠폰 적용 결제" 테스트 — 어제 실행에서 쿠폰을 이미 써 버렸다
  • "환불 요청" 테스트 — 다른 팀원이 수동 테스트하다 그 주문을 취소했다
  • "VIP 회원 전용 혜택" 테스트 — VIP 등급 계정의 등급이 월초 배치로 내려갔다

모두 제품 결함이 아니고, 테스트 코드 결함도 아니다. 테스트가 기대한 사전 조건(pre-condition) 이 환경에 없었을 뿐이다. 모듈 7 레슨 2에서 테스트 데이터 관리의 세 접근(합성 데이터·마스킹·시딩과 리셋)과 데이터 격리를 배웠다. 이 레슨은 그중 E2E 자동화에서 가장 실용적인 방식 — 테스트가 스스로 필요한 데이터를 API로 만들고 끝나면 치우는 구조 — 를 Playwright로 설계한다. 모듈 13 레슨 6의 "불안정한 테스트의 원인" 중 데이터·환경 범주를 구조적으로 없애는 방법이다.

전체 파이프라인 — 다섯 단계

┌──────────────────┐   ┌────────────────┐   ┌────────────────┐   ┌──────────────┐   ┌────────────┐
│ 1. Pre-condition │ → │ 2. Fixture Call│ → │ 3. Context     │ → │ 4. E2E 실행   │ → │ 5. Clean-up│
│    추출           │   │  API로 상태 생성 │   │    Inject      │   │  UI 시나리오   │   │  정리·취소  │
│ TC 메타데이터의   │   │  주문·쿠폰·계정  │   │  생성된 ID·토큰 │   │  화면 조작과   │   │  생성한 것만 │
│ fixture_payload  │   │  (100% API)     │   │  을 테스트에 전달│   │  단언          │   │  되돌림      │
└──────────────────┘   └────────────────┘   └────────────────┘   └──────────────┘   └────────────┘
         │                      │                                       │
         │                      └─ 실패(4xx/5xx) → [환경 문제]            └─ 실패 → [셀렉터 오류] 또는 [실제 버그]
단계하는 일설계 포인트
1. Pre-condition 추출테스트 케이스가 필요로 하는 상태를 선언에서 읽는다테스트 코드 안에 흩어두지 않고 메타데이터(fixture_payload) 로
2. Fixture Call백엔드 API로 필요한 상태를 만든다UI를 쓰지 않는다. 빠르고 결정적인 API만
3. Context Inject만든 리소스의 ID·토큰·URL을 테스트에 전달테스트는 "어떤 주문"이 아니라 "이 주문"을 다룬다
4. E2E 실행시험하려는 화면 흐름만 UI로 수행준비 과정이 빠져 테스트가 짧고 목적이 선명해진다
5. Clean-up테스트가 만든 리소스만 정리·취소실패해도 반드시 실행. 식별 접두사로 남은 것도 찾을 수 있게

화살표 아래 두 갈래가 트리아지의 출발점이다. 어느 단계에서 실패했는가 가 곧 실패의 성격을 알려준다(마지막 절).

Playwright Custom Fixture — test.extend

Playwright의 test.extend 는 테스트에 주입할 fixture 를 정의한다. fixture 함수는 use() 호출 앞에서 준비 하고, use() 로 값을 테스트에 넘기고, use() 가 끝난 뒤에서 정리 한다. 테스트가 실패해도 정리 코드는 실행된다. 이 구조가 위 파이프라인의 2·3·5단계와 정확히 대응한다.

먼저 API 클라이언트를 만든다. Playwright의 request fixture(APIRequestContext)를 쓰면 별도 HTTP 라이브러리 없이 같은 설정(baseURL·헤더)으로 API를 부를 수 있다.

// fixtures/api-client.ts
import { APIRequestContext, APIResponse } from '@playwright/test';
 
export class FixtureApiError extends Error {
  constructor(
    readonly step: string,
    readonly status: number,
    readonly body: string,
  ) {
    super(`[fixture:${step}] HTTP ${status} — ${body.slice(0, 300)}`);
    this.name = 'FixtureApiError';
  }
}
 
async function ensureOk(step: string, res: APIResponse): Promise<APIResponse> {
  if (!res.ok()) throw new FixtureApiError(step, res.status(), await res.text());
  return res;
}
 
export interface CreateOrderPayload {
  productSku: string;
  quantity: number;
  status: 'PAID' | 'SHIPPING' | 'DELIVERED';
  couponCode?: string;
}
 
export interface CreatedOrder {
  orderId: string;
  orderNo: string;
  status: string;
}
 
export class FixtureApi {
  constructor(
    private readonly api: APIRequestContext,
    private readonly runTag: string,          // 멱등성·정리용 식별 접두사
  ) {}
 
  async createOrder(userId: string, payload: CreateOrderPayload): Promise<CreatedOrder> {
    const res = await ensureOk('createOrder', await this.api.post('/qa-fixtures/orders', {
      data: { userId, ...payload, externalRef: `${this.runTag}-order` },
      headers: { 'Idempotency-Key': `${this.runTag}-order` },
    }));
    return res.json();
  }
 
  async cancelOrder(orderId: string): Promise<void> {
    const res = await this.api.post(`/qa-fixtures/orders/${orderId}/cancel`);
    // 이미 정리된 경우(404·409)는 정리 성공으로 본다 — 정리는 멱등해야 한다
    if (!res.ok() && ![404, 409].includes(res.status())) {
      await ensureOk('cancelOrder', res);
    }
  }
 
  async cleanupByTag(): Promise<void> {
    // 테스트가 중간에 죽어 개별 정리를 못 했을 때를 위한 안전망
    await this.api.delete(`/qa-fixtures/resources?tagPrefix=${encodeURIComponent(this.runTag)}`);
  }
}

엔드포인트 경로 /qa-fixtures/... 는 예시다. 실제로는 제품의 공개 API를 그대로 쓰거나, 시험 환경에서만 열리는 fixture 전용 API(주문 상태를 바로 SHIPPING으로 만드는 등)를 개발팀과 합의해 만든다. 후자가 훨씬 빠르고 결정적이지만, 운영 환경에서는 절대 열리지 않게 배포 설정으로 막아야 한다(마지막 흔한 실수 참조).

이제 fixture를 정의한다.

// fixtures/test.ts
import { test as base, expect, request as pwRequest, APIRequestContext } from '@playwright/test';
import { FixtureApi, CreatedOrder, CreateOrderPayload, FixtureApiError } from './api-client';
 
type FixturePayload = {
  user: 'buyer' | 'vip';
  order?: CreateOrderPayload;
};
 
type TestOptions = {
  fixturePayload: FixturePayload;     // 테스트마다 test.use() 로 지정 — TC 메타데이터에서 옴
};
 
type TestFixtures = {
  runTag: string;
  fixtureApi: FixtureApi;
  preparedOrder: CreatedOrder;
};
 
type WorkerFixtures = {
  serviceApi: APIRequestContext;      // 서비스별 API 토큰을 미리 주입한 컨텍스트
};
 
export const test = base.extend<TestOptions & TestFixtures, WorkerFixtures>({
  fixturePayload: [{ user: 'buyer' }, { option: true }],
 
  // 워커당 한 번: 서비스 API 토큰을 헤더에 주입 (로그인 UI 우회)
  serviceApi: [async ({}, use) => {
    const ctx = await pwRequest.newContext({
      baseURL: process.env.API_BASE_URL,
      extraHTTPHeaders: { Authorization: `Bearer ${process.env.QA_SERVICE_TOKEN}` },
    });
    await use(ctx);
    await ctx.dispose();
  }, { scope: 'worker' }],
 
  // 테스트마다 고유 식별 접두사 — 병렬 실행·재실행에서도 충돌하지 않게
  runTag: async ({}, use, testInfo) => {
    const runId = process.env.CI_RUN_ID ?? 'local';
    await use(`qa-e2e-${runId}-${testInfo.workerIndex}-${testInfo.testId}-r${testInfo.retry}`);
  },
 
  fixtureApi: async ({ serviceApi, runTag }, use) => {
    const api = new FixtureApi(serviceApi, runTag);
    await use(api);
    await api.cleanupByTag();          // 5) 안전망 정리
  },
 
  // 2) Fixture Call → 3) Context Inject → 5) Clean-up
  preparedOrder: async ({ fixtureApi, fixturePayload }, use, testInfo) => {
    if (!fixturePayload.order) throw new Error('fixture_payload.order 가 정의되지 않았습니다');
 
    testInfo.annotations.push({ type: 'phase', description: 'fixture' });
    const userId = testInfo.project.metadata?.users?.[fixturePayload.user] ?? fixturePayload.user;
    const order = await fixtureApi.createOrder(userId, fixturePayload.order);
 
    testInfo.annotations.push({ type: 'phase', description: 'ui' });
    await use(order);                  // 4) 테스트 본문 실행
 
    await fixtureApi.cancelOrder(order.orderId);
  },
});
 
export { expect, FixtureApiError };

storageState 로 로그인 상태를 미리 만드는 것도 같은 원칙이다. 브라우저 테스트가 로그인 화면을 거치지 않도록, 사용자별 토큰·쿠키를 API로 발급받아 저장 상태로 주입 한다.

// global-setup.ts — 사용자별 로그인 상태를 API 로 만든다 (로그인 UI 를 타지 않음)
import { request, FullConfig } from '@playwright/test';
 
export default async function globalSetup(config: FullConfig) {
  for (const user of ['buyer', 'vip'] as const) {
    const ctx = await request.newContext({ baseURL: process.env.API_BASE_URL });
    const res = await ctx.post('/auth/test-token', {
      data: { account: process.env[`QA_${user.toUpperCase()}_ACCOUNT`] },
      headers: { Authorization: `Bearer ${process.env.QA_SERVICE_TOKEN}` },
    });
    if (!res.ok()) throw new Error(`[fixture:auth] HTTP ${res.status()}`);
    await ctx.storageState({ path: `.auth/${user}.json` });   // 발급된 쿠키를 저장
    await ctx.dispose();
  }
}

테스트는 이렇게 짧아진다.

// tests/order-shipping.spec.ts
import { test, expect } from '../fixtures/test';
 
test.use({
  storageState: '.auth/buyer.json',
  fixturePayload: {
    user: 'buyer',
    order: { productSku: 'SKU-QA-001', quantity: 2, status: 'SHIPPING' },
  },
});
 
test('배송 중 주문은 상세 화면에서 배송 조회 버튼이 보인다', async ({ page, preparedOrder }) => {
  await page.goto(`/orders/${preparedOrder.orderId}`);
  await expect(page.getByRole('heading', { name: preparedOrder.orderNo })).toBeVisible();
  await expect(page.getByRole('button', { name: '배송 조회' })).toBeVisible();
  await expect(page.getByRole('button', { name: '주문 취소' })).toBeHidden();
});

테스트 본문에는 시험하려는 것만 남았다. 주문을 만들고 결제하고 배송 상태로 바꾸는 과정은 없다. 그 과정은 다른 테스트(또는 API 테스트)의 몫이다.

설계 원칙 ① — UI로 전처리 데이터를 만들지 않는다 (100% API)

전처리를 UI로 하면 세 가지가 무너진다.

문제설명
속도주문 하나를 만들려고 상품 검색·장바구니·결제 화면을 지나면 테스트 하나가 수십 초 늘어난다
실패 원인 오염"배송 조회 버튼" 테스트가 결제 화면 셀렉터 변경 때문에 실패한다. 시험 대상이 아닌 곳의 변경이 시험을 막는다
불안정성UI 단계가 많을수록 플래키 확률이 곱해진다 (모듈 13 레슨 6)

그래서 원칙은 사전 데이터는 100% API로 다. UI는 시험 대상 흐름에만 쓴다. 모듈 11 레슨 1의 자동화 피라미드에서 E2E를 적게 두라는 원칙이 한 테스트 안에서도 적용되는 셈이다.

API가 없어서 UI로 만들 수밖에 없는 데이터가 있다면, 그것 자체를 개발팀에 요청할 테스트 용이성(testability) 항목 으로 기록한다.

설계 원칙 ② — fixture_payload로 파라미터를 중앙 관리한다

필요한 사전 조건을 테스트 코드마다 하드코딩하면, 상품 SKU가 바뀌거나 쿠폰 정책이 바뀔 때 수십 개 파일을 고쳐야 한다. 테스트 케이스 메타데이터(테스트 관리 도구의 사용자 정의 필드, 또는 저장소의 TC 정의 파일)에 fixture_payload 필드를 두고 한 곳에서 관리한다.

# testcases/order/TC-ORD-021.yaml
id: TC-ORD-021
title: 배송 중 주문은 상세 화면에서 배송 조회 버튼이 보인다
priority: high
automation:
  spec: tests/order-shipping.spec.ts
fixture_payload:
  user: buyer
  order:
    productSku: SKU-QA-001      # QA 전용 상품 — 재고 무제한, 가격 고정
    quantity: 2
    status: SHIPPING

테스트는 메타데이터를 읽어 test.use() 에 넣는다.

// fixtures/load-payload.ts
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
 
export function fixturePayloadOf(tcId: string) {
  const [area] = tcId.replace('TC-', '').split('-');
  const doc = parse(readFileSync(`testcases/${area.toLowerCase()}/${tcId}.yaml`, 'utf8'));
  if (!doc.fixture_payload) throw new Error(`${tcId}: fixture_payload 없음`);
  return doc.fixture_payload;
}
 
// tests/order-shipping.spec.ts 에서
// test.use({ storageState: '.auth/buyer.json', fixturePayload: fixturePayloadOf('TC-ORD-021') });

얻는 것은 세 가지다. 정책 변경을 한 곳에서 반영하고, 테스트 관리 도구에서 "이 TC가 어떤 데이터를 필요로 하는가"를 코드 없이 읽을 수 있고, 같은 payload를 수동 테스트 준비 스크립트 로도 재사용할 수 있다.

설계 원칙 ③ — 식별 접두사로 멱등성을 보장한다

E2E는 병렬로 돌고, 실패하면 재시도하고, 같은 커밋으로 여러 번 돈다. 그때마다 데이터가 겹치면 테스트끼리 서로의 주문을 취소하거나, 같은 쿠폰을 동시에 쓰거나, 정리되지 않은 데이터가 쌓인다.

runTag 가 이 문제를 푼다.

qa-e2e-{CI 실행 ID}-{워커 번호}-{테스트 ID}-r{재시도 횟수}
예: qa-e2e-8812345-3-a1b2c3d4-r0
쓰임효과
생성 요청의 Idempotency-Key네트워크 재시도로 같은 요청이 두 번 가도 주문은 하나 (모듈 41 레슨 2의 멱등성)
리소스의 외부 참조(externalRef)누가 만든 데이터인지 운영자가 즉시 알아봄
병렬 실행 격리워커·테스트·재시도마다 달라 서로 충돌하지 않음
안전망 정리테스트가 비정상 종료해도 접두사로 남은 리소스를 일괄 삭제
야간 청소qa-e2e- 로 시작하고 24시간 지난 리소스를 배치로 정리

재시도 번호(r0, r1)를 넣은 이유가 있다. 첫 시도에서 정리에 실패한 리소스가 남아 있어도 재시도는 새 리소스 로 시작한다. 재시도가 첫 시도의 잔여물을 재사용하면, 첫 시도의 실패 원인이 재시도에 그대로 이어진다.

설계 원칙 ④ — 서비스별 API 토큰을 미리 주입해 로그인을 우회한다

모든 테스트가 로그인 화면을 거치면 로그인 화면 하나의 변경이 전체 스위트 를 멈춘다. 로그인은 로그인 테스트에서만 UI로 시험하고, 나머지는 토큰을 주입한다.

대상방식
Fixture API 호출워커 범위 serviceApi 에 QA 서비스 토큰을 헤더로 주입 (위 코드)
브라우저 세션globalSetup 에서 API로 사용자 세션을 발급받아 storageState 로 저장, test.use({ storageState }) 로 주입
마이크로서비스별 토큰서비스마다 인증 체계가 다르면 서비스별 APIRequestContext 를 워커 fixture로 분리

토큰은 CI 비밀 저장소 에서 환경 변수로 주입하고 저장소에 커밋하지 않는다. .auth/*.json 도 .gitignore 에 넣는다. QA 서비스 토큰은 시험 환경 전용 권한 으로 발급받고, 운영 환경에서는 유효하지 않게 한다.

QMS·트리아지 연동 — 실패 단계로 자동 분류한다

파이프라인을 단계로 나눈 보상이 여기서 나온다. 테스트가 어느 단계에서 실패했는지 알면, 실패의 성격을 사람이 로그를 읽기 전에 자동으로 1차 분류할 수 있다.

실패 단계신호자동 분류다음 동작
Fixture 단계 (API 4xx/5xx, 토큰 발급 실패)FixtureApiError 또는 phase=fixture 에서 실패[환경 문제]환경 담당·당번 알림, 결함 등록하지 않음, 재실행 대상
UI 단계 — 요소를 찾지 못함로케이터 대기 타임아웃, strict mode 위반[셀렉터 오류]자동화 담당 검토 (셀프힐링 대상 여부 — 모듈 18a 레슨 15)
UI 단계 — 요소는 찾았으나 기대와 다름expect 단언 실패[실제 버그] 후보결함 초안 생성, 사람 확인 후 등록
Clean-up 단계정리 API 실패[환경 문제] (테스트 결과는 유지)정리 실패 목록 적재, 야간 청소 대상

FixtureApiError 를 따로 만든 이유가 이것이다. 에러 이름만으로 단계가 구분된다. 분류는 커스텀 리포터 로 구현한다.

// reporters/triage-reporter.ts
import type { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
 
type Triage = '환경 문제' | '셀렉터 오류' | '실제 버그' | '미분류';
 
export function classify(result: TestResult): Triage {
  const err = result.errors[0];
  const msg = `${err?.message ?? ''}\n${err?.stack ?? ''}`;
  const lastPhase = [...result.annotations].reverse().find((a) => a.type === 'phase')?.description;
 
  // 1) Fixture 단계 실패 — API 4xx/5xx 또는 fixture 단계에서 멈춤
  if (/FixtureApiError|\[fixture:/.test(msg) || lastPhase === 'fixture') return '환경 문제';
 
  // 2) UI 단계 — 요소를 찾지 못함
  if (/strict mode violation/.test(msg)) return '셀렉터 오류';
  if (/Timeout \d+ms exceeded/.test(msg) && /waiting for (locator|getBy)/.test(msg)) return '셀렉터 오류';
 
  // 3) UI 단계 — 요소는 있으나 기대와 다름
  if (/expect\(.*\)\.to|toHave|toBe|toContain/.test(msg)) return '실제 버그';
 
  return '미분류';
}
 
export default class TriageReporter implements Reporter {
  private rows: { title: string; triage: Triage; message: string }[] = [];
 
  onTestEnd(test: TestCase, result: TestResult) {
    if (result.status === 'passed' || result.status === 'skipped') return;
    this.rows.push({
      title: test.titlePath().join(' > '),
      triage: classify(result),
      message: (result.errors[0]?.message ?? '').split('\n')[0].slice(0, 200),
    });
  }
 
  async onEnd() {
    // 여기서 QMS·결함 추적 도구 API 로 전송하거나 파일로 저장한다
    // '실제 버그' 는 결함 "초안"으로만 만든다 — 등록은 사람이 확인한 뒤
    console.table(this.rows);
  }
}
// playwright.config.ts (발췌)
import { defineConfig } from '@playwright/test';
 
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  reporter: [['html'], ['./reporters/triage-reporter.ts']],
  use: { baseURL: process.env.WEB_BASE_URL },
});

이 분류는 1차 분류 다. 세 가지를 기억하자.

  • [실제 버그] 는 후보일 뿐이다. 단언 실패는 기대값이 틀린 테스트 결함일 수도 있다. 결함은 초안으로 만들고 사람이 등록한다(모듈 38 레슨 5 — 자동 등록은 결함 추세를 오염시킨다)
  • [환경 문제] 가 반복되면 그것도 결함이다. 같은 fixture API가 매일 500을 내면 시험 환경 결함이고, 개발팀 과제다. 환경 문제 비율을 지표로 본다(모듈 38 레슨 6)
  • 분류 규칙은 Playwright 메시지 형식에 의존한다. 버전을 올릴 때 분류기 자체의 회귀 시험(각 분류를 일부러 만드는 테스트)을 돌린다

실무에서 왜 필요한가

E2E 스위트가 커질수록 실패의 상당 부분이 제품 결함이 아니라 데이터와 환경 에서 나온다. 매일 아침 QA가 실패 목록을 열어 "이건 데이터 문제, 이건 환경, 이건 진짜"를 손으로 가르는 동안, 진짜 결함은 소음 속에 묻힌다. 그리고 팀은 E2E 결과를 믿지 않게 된다.

API fixture와 단계별 트리아지는 이 두 문제를 같이 푼다. 데이터 부재로 인한 실패가 구조적으로 줄고, 남은 실패는 어느 단계에서 났는지로 자동 1차 분류 된다. 모듈 38의 AI 검증 파이프라인도 이 구조 위에서만 작동한다. 판정자가 "데이터가 없어서 실패한 테스트"를 결함으로 판정하면 게이트 전체를 믿을 수 없기 때문이다. 채용 공고의 "E2E·API 테스트 자동화를 구축·운영해 본 경험"에서 운영 이 가리키는 것이 바로 이 부분이다.

실습 과제

과제 1 — fixture_payload 설계 (15분)

본인이 아는 서비스(또는 가상의 쇼핑몰)에서 E2E 테스트 케이스 다섯 개를 고르고, 각 케이스의 사전 조건을 fixture_payload 로 쓴다. 각 payload를 만들 API가 있는지 표시하고, API가 없어 UI로만 만들 수 있는 데이터는 개발팀에 요청할 테스트 용이성 항목으로 따로 적는다.

과제 2 — Custom Fixture 구현 (30분)

공개 연습용 API(예: 할 일 목록 API를 제공하는 연습 사이트나 로컬 목 서버)를 대상으로, 테스트 전에 API로 항목을 만들고 테스트에 ID를 주입하고 테스트 후 삭제하는 fixture를 test.extend 로 구현한다. 식별 접두사를 넣고, 테스트를 일부러 실패시켜도 정리가 실행되는지 확인한다.

과제 3 — 트리아지 분류기 검증 (20분)

본문의 classify 를 가져와 네 가지 실패를 일부러 만든다 — fixture API가 500을 반환, 존재하지 않는 셀렉터, 여러 요소에 매칭되는 셀렉터, 틀린 단언. 각 실패가 기대한 분류로 나오는지 확인하고, 잘못 분류된 경우 규칙을 고친다.

자가 체크리스트

  • 데이터 부재로 인한 E2E 실패가 제품·테스트 코드 결함과 어떻게 다른지 설명할 수 있다
  • Pre-condition 추출·Fixture Call·Context Inject·E2E 실행·Clean-up 다섯 단계를 설명할 수 있다
  • test.extend 로 use() 앞에서 준비하고 뒤에서 정리하는 fixture를 구현할 수 있다
  • 워커 범위 fixture로 서비스 API 토큰을 주입하고 storageState 로 로그인을 우회할 수 있다
  • 전처리 데이터를 UI가 아니라 100% API로 만들어야 하는 이유 세 가지를 말할 수 있다
  • fixture_payload 로 사전 조건 파라미터를 TC 메타데이터에서 중앙 관리할 수 있다
  • 실행 ID·워커·테스트 ID·재시도 번호로 식별 접두사를 만들어 멱등성과 정리를 보장할 수 있다
  • Fixture 단계 실패는 [환경 문제], UI 단계 실패는 [셀렉터 오류] 또는 [실제 버그]로 자동 분류하는 리포터를 만들 수 있다
  • 자동 분류가 1차 분류이고 결함 등록은 사람이 확인해야 하는 이유를 안다

흔한 실수

사전 데이터를 UI로 만든다. 시험 대상이 아닌 화면의 변경이 시험을 막는다.

스테이징에 "항상 있는" 공유 데이터에 기댄다. 다른 사람·다른 테스트가 바꾼다. 테스트가 스스로 만든다.

정리를 테스트 본문 끝에 둔다. 실패하면 실행되지 않는다. fixture의 use() 뒤에 둔다.

재시도에서 첫 시도의 데이터를 재사용한다. 첫 시도의 실패 원인이 이어진다. 재시도 번호를 접두사에 넣는다.

fixture 전용 API가 운영에서도 열린다. 주문 상태를 바로 바꾸는 API가 운영에 노출되면 보안 사고다. 배포 설정으로 시험 환경에서만 활성화하고 이를 시험한다.

토큰과 storageState 파일을 커밋한다. CI 비밀 저장소와 .gitignore 로 관리한다.

환경 문제로 분류된 실패를 영원히 무시한다. 반복되는 환경 문제는 시험 환경 결함이다.

참고 자료

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

E2E — 끝에서 끝까지
화면 한 조각이 아니라 사용자가 밟는 전체 흐름을 처음부터 끝까지 한 번에 돌려 보는 시험.
API — 응용 프로그램 인터페이스
프로그램끼리 주고받는 창구. 사람이 화면 버튼을 누르는 대신, 프로그램이 정해진 주소로 요청을 보내 답을 받는다.
UI — 사용자 인터페이스
사람이 직접 보고 누르는 부분 — 화면, 버튼, 입력창.
ID — 식별자
무엇 하나를 다른 것과 구별하기 위해 붙인 이름이나 번호.
URL — 인터넷 주소
웹에서 무언가의 위치를 가리키는 주소 한 줄.
HTTP — 하이퍼텍스트 전송 규약
웹에서 요청과 응답을 주고받는 약속. "무엇을 달라"고 보내면 "여기 있다" 또는 "그런 건 없다"가 돌아온다.
TC — 테스트 케이스
무엇을 어떻게 해보고 무엇이 나와야 하는지를 한 건으로 적어 둔 것. 시험 문제 한 문항에 해당한다.
QA — 품질보증
만든 것이 쓸 만한지 확인하고, 애초에 덜 망가지게 만드는 방법을 정하는 일. "테스트"는 그 일의 일부다.
CI — 지속적 통합
코드를 올릴 때마다 자동으로 합쳐서 빌드하고 시험까지 돌리는 방식.
AI — 인공지능
사람이 규칙을 일일이 적지 않아도 데이터에서 규칙을 찾아 판단하는 소프트웨어.
POM — 페이지 객체 모델
화면마다 "이 화면에서 할 수 있는 일"을 모아 둔 코드 조각을 만들어 두고, 시험은 그것만 호출하게 하는 설계.

용어집 전체 보기

이 레슨의 실습

실습은 별도 저장소에서 진행합니다. 새 탭으로 열립니다.