Navigation

1. 범위와 배치

검증할 계약에 맞는 가장 작은 실행 범위를 고르고 층별 책임과 소유 위치를 정합니다. 프로젝트의 테스트 추가 정책과 러너 수집 경계를 유지합니다.

1.1 신뢰를 얻을 수 있는 가장 작은 범위를 선택합니다

검증에 필요한 실제 경계를 유지하면서 실행 비용과 실패 원인을 줄입니다.

검증할 계약을 먼저 적고 그 계약이 실제로 실행되는 가장 작은 범위를 선택합니다. 러너 이름이나 화면 수로 테스트 층을 정하지 않습니다.

실행 경계작성 기준
unit계산, 변환, 정렬, 입력 판정을 실제 함수에 인자로 전달합니다
integration응답, props, URL 상태에 따른 구역과 실제 provider의 연결을 봅니다
앱 E2E앱 진입, 라우팅, 세션 전달, 파일 처리의 실제 여정을 통과합니다
실제 시스템 E2E서버 인증, 저장까지 주장할 때 해당 서버와 저장 경계를 포함합니다

레이아웃, 측정, 스크롤, canvas처럼 브라우저 동작이 계약이면 실제 브라우저를 사용합니다. 브라우저에서 story를 mount해도 앱 전체가 실행되는 것은 아니므로 integration 경계로 봅니다. 화면 하나의 진입, 세션 연결도 앱 경계를 검증하면 E2E이며 화면 수로 나누지 않습니다.

함수의 입력 조합은 unit, 구역의 로딩, 빈 값, 실패와 조작은 integration에 둡니다. 앱 E2E에는 대표 여정과 연결이 끊겼을 때 실패할 사례를 남깁니다. mock 서버를 쓰는 앱 E2E는 실제 인증, DB 저장까지 증명하지 않습니다. 단순 래퍼나 서드파티 자체의 동작을 반복하는 테스트는 추가하지 않습니다.

참고: 공식 문서.

브라우저 전체에서 숫자 포맷만 확인합니다.

test/orders/orders.e2e.spec.tsts
import { expect, test } from "@playwright/test";

test("수량은 소수 두 자리로 표시된다", async ({ page }) => {
	/**
	 * GIVEN
	 * 수량 입력 화면
	 */
	await page.goto("/orders/new");

	/**
	 * WHEN
	 * 수량 1234 입력
	 */
	await page.getByLabel("수량").fill("1234");

	/**
	 * THEN
	 * 천 단위 구분과 소수 두 자리
	 */
	await expect(page.getByRole("status")).toHaveText("1,234.00");
});

실제 포맷 함수만 실행합니다.

src/util/number/format-quantity.test.tsts
import { expect, test } from "vitest";

import { formatQuantity } from "@/util/number/format-quantity";

test("수량은 소수 두 자리로 표시된다", () => {
	/**
	 * GIVEN
	 * 소수 표기를 확인할 수량
	 */
	const quantity = 1234;

	/**
	 * WHEN
	 * 수량의 표시 문자열 계산
	 */
	const label = formatQuantity(quantity);

	/**
	 * THEN
	 * 천 단위 구분과 소수 두 자리
	 */
	expect(label).toBe("1,234.00");
});

1.2 상세한 경우의 수와 상위 연결 검증을 나눕니다

중복 비용을 줄이면서 함수와 화면 사이의 연결 누락을 검출합니다.

상세한 입력 조합은 주된 책임을 가진 층에서 검증하고 상위 층에는 실제 연결을 확인하는 대표 사례를 둡니다. 같은 입력을 쓴다는 이유만으로 서로 다른 계약의 테스트를 삭제하지 않습니다.

날짜 보정 함수는 보정 결과를, 화면 통합 테스트는 URL의 날짜가 실제 입력란에 반영되는 연결을 확인합니다. 함수가 맞아도 바인딩이 틀릴 수 있으므로 두 사례를 유지합니다. 상위 층에서 함수의 전체 경우의 수를 다시 순회하는 대신 연결이 끊겼을 때 실패할 사례를 고릅니다.

참고: 공식 문서.

화면에서 함수의 입력 조합을 모두 반복합니다.

test/reports/reports.integration.spec.tsts
import { expect, test } from "@playwright/test";

const ranges = [
	{ start: "2026-03-01", end: "2026-01-01" },
	{ start: "2026-04-01", end: "2026-02-01" },
	{ start: "2026-05-01", end: "2026-03-01" },
];

for (const range of ranges) {
	test(`뒤집힌 기간 ${range.start}를 보정한다`, async ({ page }) => {
		/**
		 * GIVEN
		 * 입력 조합마다 다시 여는 전체 화면
		 */
		const path = `/reports?startDate=${range.start}&endDate=${range.end}`;

		/**
		 * WHEN
		 * 날짜 보정 함수를 화면에서 반복 실행
		 */
		await page.goto(path);

		/**
		 * THEN
		 * 같은 함수의 입력 표를 화면에서 재검사
		 */
		await expect(page.getByLabel("시작일")).toHaveValue(range.end);
		await expect(page.getByLabel("종료일")).toHaveValue(range.start);
	});
}

화면에서는 URL과 입력란의 연결을 대표 사례로 확인합니다.

test/reports/reports.integration.spec.tsts
import { expect, test } from "@playwright/test";

test("뒤집힌 URL 기간이 입력란에 보정돼 표시된다", async ({ page }) => {
	/**
	 * GIVEN
	 * 뒤집힌 기간이 있는 URL
	 */
	const path = "/reports?startDate=2026-03-01&endDate=2026-01-01";

	/**
	 * WHEN
	 * URL로 화면 진입
	 */
	await page.goto(path);

	/**
	 * THEN
	 * 보정된 날짜와 입력란의 실제 연결
	 */
	await expect(page.getByLabel("시작일")).toHaveValue("2026-01-01");
	await expect(page.getByLabel("종료일")).toHaveValue("2026-03-01");
});

1.3 소유 대상을 찾기 쉽게 배치하고 러너 수집 범위를 분리합니다

테스트 위치를 예측할 수 있고 다른 러너의 파일이 잘못 실행되지 않습니다.

함수 테스트는 대상 함수 가까이에, 화면과 여정 테스트는 소유 화면을 찾기 쉬운 위치에 둡니다. 여러 러너가 있으면 파일 이름과 수집 패턴을 함께 정해 서로의 테스트를 실행하지 않게 합니다.

파일배치 기준
함수 테스트, 매퍼의 골든 입력대상 소유자 가까이에 둡니다
화면, 앱 여정프로젝트의 화면별 테스트 구조를 따릅니다
fixture, story, page object, 기준값사용하는 테스트의 지원 코드로 구분합니다

IA 번호, 케이스 ID, 별칭, 실제 폴더 이름은 프로젝트 문서가 정합니다. 단순 이동을 이유로 무관한 테스트와 프로덕션 파일을 재배치하지 않습니다.

참고: 공식 문서.

역할이 섞여 어떤 러너가 읽는지 알기 어렵습니다.

test/
  format-quantity.spec.ts
  reports.spec.ts
  helpers.spec.ts

Vitest include: test/**/*.spec.ts
Playwright testMatch: test/**/*.spec.ts

파일 위치와 수집 범위를 함께 정합니다.

src/util/number/format-quantity.test.ts
test/reports/reports.integration.spec.ts
test/reports/reports.e2e.spec.ts
test/_fixture/reports-page.ts

Vitest include: src/**/*.test.ts
Playwright testMatch: test/**/*.integration.spec.ts, test/**/*.e2e.spec.ts

1.4 테스트 층과 smoke, sanity의 선별 목적을 구분합니다

실행 경계와 선별 목적을 나눠 필요한 검증을 빠짐없이 선택합니다.

unit, integration, E2E는 실행 경계이고 smoke, sanity, regression은 실행 목적과 선택 범위입니다. 이름 하나를 같은 층 목록에 섞거나 빠르게 끝났다는 이유로 smoke라고 부르지 않습니다. 아래 선별 기준을 팀의 기본값으로 쓰고 프로젝트의 기존 정의가 다르면 그 계약을 명시합니다.

목적고르는 기준통과가 말하는 범위
smoke배포한 산출물의 진입, 핵심 자원, 대표 흐름다음 검증을 진행할 최소 상태
sanity변경점, 결함 수정과 직접 연결된 인접 흐름해당 변경의 좁은 정상 여부
regression기존 계약과 실패 이력을 위험에 따라 선택실행한 기존 기능의 회귀 여부

smoke는 모든 화면을 순회하지 않고, 배포 환경에서 실행할 수 있는 안전한 대표 사례를 고릅니다. sanity는 수정 사례 하나에 그치지 않고 끊어질 수 있는 연결과 인접 경계도 포함합니다. 골든과 시각 비교는 비교 방식, negative는 시나리오의 종류이므로 필요한 층에서 함께 씁니다.

태그와 우선순위는 별개입니다. 중요도가 높다고 모두 smoke로 지정하지 않습니다. 기존 사례를 태그나 명시적 목록으로 골라 실행하고 복사해 별도 테스트를 만들지 않습니다. 선별 결과가 0건이면 통과로 보고하지 않으며, mock과 실제 서버의 실행 범위도 함께 적습니다.

참고: Playwright 태그와 annotation.

순수 포맷 검사를 배포 smoke의 근거로 삼습니다.

src/util/number/format-quantity.test.tsts
import { expect, test } from "vitest";

import { formatQuantity } from "@/util/number/format-quantity";

test("배포 smoke: 수량을 표시한다", () => {
	/**
	 * GIVEN
	 * 일반 수량 입력
	 */
	const quantity = 1234;

	/**
	 * WHEN
	 * 포맷 함수만 실행
	 */
	const label = formatQuantity(quantity);

	/**
	 * THEN
	 * 배포 산출물을 거치지 않은 함수 결과
	 */
	expect(label).toBe("1,234.00");
});

실제 앱의 대표 진입 사례를 smoke로 고릅니다.

test/orders/orders.e2e.spec.tsts
import { expect, test } from "@playwright/test";

// baseURL: 검증할 배포 산출물. fixture에서 세션과 읽기 전용 order-1 데이터를 준비
test("주문 화면에 진입하면 대표 목록을 조회한다", { tag: "@smoke" }, async ({ page }) => {
	/**
	 * GIVEN
	 * 배포 환경의 읽기 전용 주문 목록 경로
	 */
	const path = "/orders";

	/**
	 * WHEN
	 * 실제 앱의 주문 화면 진입
	 */
	await page.goto(path);

	/**
	 * THEN
	 * 앱 초기화와 대표 데이터 조회의 연결
	 */
	await expect(page.getByRole("heading", { name: "주문", exact: true })).toBeVisible();
	await expect(page.getByRole("table", { name: "주문 목록" }).getByRole("cell", { name: "order-1", exact: true })).toBeVisible();
});

같은 사례를 태그로 선별하고 수집 결과를 먼저 확인합니다.

# test/README.md: 프로젝트가 정한 선택 규약
# smoke: 배포 산출물의 읽기 전용 대표 진입. 태그가 있는 기존 사례만 실행
npx playwright test --project=e2e --grep @smoke --list
npx playwright test --project=e2e --grep @smoke

# sanity: 이번 수정의 사례 ID와 인접 흐름을 선택. 별도 테스트 파일을 복사하지 않음
npx playwright test --project=integration --grep 'ORD-014|ORD-015' --list
npx playwright test --project=integration --grep 'ORD-014|ORD-015'

# 수집 0건은 통과가 아님. grep, project, 실제 실행 환경을 결과에 함께 기록