자연어를 E2E 테스트로 변환하는 Momentic, testRigor, Functionize와 DOM 변경에 자동 적응하는 셀프 힐링 기능, Playwright와 AI를 결합한 실전 E2E 테스트 자동화를 다룹니다.
End-to-End(E2E) 테스트는 사용자 관점에서 애플리케이션 전체 흐름을 검증합니다. 가장 현실적인 테스트이지만, 동시에 가장 많은 문제를 안고 있습니다.
| 도전 과제 | 설명 |
|---|---|
| 작성 비용 | 셀렉터 선택, 대기 조건 설정 등 수동 작업이 많음 |
| 유지보수 | UI 변경 시 셀렉터가 깨지는 빈도가 높음 |
| 플레이키 테스트 | 타이밍, 네트워크, 렌더링 순서에 의한 비결정적 실패 |
| 실행 시간 | 브라우저 구동과 네트워크 대기로 인해 느림 |
| 환경 의존성 | 데이터베이스 상태, 외부 서비스 가용성에 영향받음 |
AI는 이 모든 도전 과제에 대해 해결책을 제시하고 있습니다.
전통적인 E2E 테스트는 셀렉터와 액션을 코드로 직접 작성해야 했습니다. 2026년의 AI 기반 도구들은 자연어 명령을 실행 가능한 테스트로 변환합니다.
Momentic은 자연어로 E2E 테스트를 작성할 수 있는 플랫폼입니다. 개발자가 코드를 작성하는 대신, 테스트 시나리오를 자연어로 기술합니다.
1. https://example.com/login 페이지로 이동합니다
2. 이메일 입력란에 "test@example.com"을 입력합니다
3. 비밀번호 입력란에 "password123"을 입력합니다
4. 로그인 버튼을 클릭합니다
5. 대시보드 페이지로 이동되는지 확인합니다
6. 환영 메시지에 "test@example.com"이 포함되어 있는지 확인합니다Momentic은 이 자연어를 해석하여 내부적으로 Playwright/Puppeteer 기반의 액션으로 변환합니다. 핵심은 DOM 요소를 CSS 셀렉터가 아닌 의미(semantic)로 찾는다는 점입니다. "이메일 입력란"이라고 기술하면, label, placeholder, aria-label 등을 종합적으로 분석하여 올바른 요소를 찾습니다.
testRigor는 비개발자도 테스트를 작성할 수 있도록 설계된 플랫폼입니다. QA 엔지니어, 제품 관리자가 직접 테스트 시나리오를 작성할 수 있습니다.
login as "test@example.com" with password "password123"
check that page contains "대시보드"
click on "새 프로젝트 만들기"
enter "AI 테스트 프로젝트" into "프로젝트 이름"
click on "생성"
check that page contains "AI 테스트 프로젝트"Functionize는 ML과 NLP를 결합한 클라우드 기반 테스트 플랫폼입니다. 사용자의 행동을 학습하여 테스트를 자동 생성하고, 애플리케이션 변경에 자동으로 적응합니다.
자연어 기반 도구를 선택할 때 고려할 점은 다음과 같습니다. (1) 한국어 지원 수준 -- 일부 도구는 영어에 최적화되어 있습니다. (2) CI 통합 용이성 -- CLI나 API를 통한 자동 실행이 가능한지 확인합니다. (3) 디버깅 지원 -- 실패 시 원인을 파악할 수 있는 로그와 스크린샷을 제공하는지 확인합니다.
2026년 현재, E2E 테스트에서 가장 성숙한 AI 기능은 Self-healing(셀프 힐링)입니다. DOM 구조가 변경되어 기존 셀렉터가 깨졌을 때, AI가 자동으로 대체 셀렉터를 찾아 테스트를 복구합니다.
셀프 힐링은 다중 속성을 분석하여 요소를 식별합니다.
| 속성 | 가중치 | 설명 |
|---|---|---|
| 텍스트 콘텐츠 | 높음 | 버튼의 텍스트, 입력란의 레이블 |
| ARIA 속성 | 높음 | aria-label, role, aria-describedby |
| data 속성 | 중간 | data-testid, data-cy 등 |
| CSS 클래스 | 낮음 | 변경 빈도가 높아 신뢰도가 낮음 |
| XPath 위치 | 낮음 | DOM 구조 변경에 취약 |
| 시각적 위치 | 중간 | 페이지 내 상대적 위치 |
셀프 힐링이 만능은 아닙니다. 다음 상황에서는 자동 복구가 어렵습니다.
셀프 힐링이 적용된 테스트는 반드시 리포트를 검토해야 합니다. AI가 "자동 복구"한 셀렉터가 정말 올바른 요소를 가리키는지 확인하지 않으면, 잘못된 요소를 테스트하는 위양성(false positive) 문제가 발생할 수 있습니다.
Playwright에 AI 기반 기능을 결합한 실전 E2E 테스트를 살펴보겠습니다.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
timeout: 30000,
retries: 2,
workers: 4,
reporter: [
["html", { open: "never" }],
["json", { outputFile: "test-results.json" }],
],
use: {
baseURL: "http://localhost:3000",
screenshot: "only-on-failure",
video: "retain-on-failure",
trace: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "mobile", use: { ...devices["iPhone 14"] } },
],
});import { Page, Locator } from "@playwright/test";
/**
* 의미 기반 요소 탐색 유틸리티
* 여러 전략을 순차적으로 시도하여 가장 적합한 요소를 반환합니다.
*/
export async function findElement(
page: Page,
description: string
): Promise<Locator> {
// 전략 1: getByRole (접근성 기반)
const roleLocator = await tryRoleLocator(page, description);
if (roleLocator) return roleLocator;
// 전략 2: getByLabel (레이블 기반)
const labelLocator = page.getByLabel(description);
if (await labelLocator.count() > 0) return labelLocator;
// 전략 3: getByPlaceholder (플레이스홀더 기반)
const placeholderLocator = page.getByPlaceholder(description);
if (await placeholderLocator.count() > 0) return placeholderLocator;
// 전략 4: getByText (텍스트 기반)
const textLocator = page.getByText(description, { exact: false });
if (await textLocator.count() > 0) return textLocator.first();
// 전략 5: data-testid 패턴 추론
const testId = description.toLowerCase().replace(/\s+/g, "-");
const testIdLocator = page.getByTestId(testId);
if (await testIdLocator.count() > 0) return testIdLocator;
throw new Error(`요소를 찾을 수 없습니다: ${description}`);
}
async function tryRoleLocator(
page: Page,
description: string
): Promise<Locator | null> {
const roleMap: Record<string, string> = {
"버튼": "button",
"링크": "link",
"입력란": "textbox",
"체크박스": "checkbox",
"드롭다운": "combobox",
};
for (const [keyword, role] of Object.entries(roleMap)) {
if (description.includes(keyword)) {
const name = description.replace(keyword, "").trim();
const locator = page.getByRole(role as any, { name });
if (await locator.count() > 0) return locator;
}
}
return null;
}import { test, expect } from "@playwright/test";
test.describe("사용자 등록 및 로그인 플로우", () => {
const testUser = {
email: `test-${Date.now()}@example.com`,
name: "테스트 사용자",
password: "SecurePass123!",
};
test("신규 사용자가 등록하고 로그인합니다", async ({ page }) => {
// 회원가입 페이지로 이동
await page.goto("/signup");
await expect(page).toHaveTitle(/회원가입/);
// 폼 입력
await page.getByLabel("이메일").fill(testUser.email);
await page.getByLabel("이름").fill(testUser.name);
await page.getByLabel("비밀번호").fill(testUser.password);
await page.getByLabel("비밀번호 확인").fill(testUser.password);
// 약관 동의
await page.getByRole("checkbox", { name: "이용약관" }).check();
await page.getByRole("checkbox", { name: "개인정보" }).check();
// 가입 버튼 클릭
await page.getByRole("button", { name: "가입하기" }).click();
// 가입 완료 확인
await expect(page.getByText("가입이 완료되었습니다")).toBeVisible();
// 로그인 페이지로 이동
await page.goto("/login");
// 로그인 수행
await page.getByLabel("이메일").fill(testUser.email);
await page.getByLabel("비밀번호").fill(testUser.password);
await page.getByRole("button", { name: "로그인" }).click();
// 대시보드 도착 확인
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByText(testUser.name)).toBeVisible();
});
test("잘못된 비밀번호로 로그인 시 에러 메시지를 표시합니다", async ({
page,
}) => {
await page.goto("/login");
await page.getByLabel("이메일").fill(testUser.email);
await page.getByLabel("비밀번호").fill("wrongpassword");
await page.getByRole("button", { name: "로그인" }).click();
await expect(
page.getByText("이메일 또는 비밀번호가 올바르지 않습니다")
).toBeVisible();
});
});Flaky Test(플레이키 테스트)는 동일한 코드에 대해 때로는 통과하고 때로는 실패하는 테스트입니다. CI 파이프라인의 신뢰도를 크게 떨어뜨리는 주요 원인입니다.
| 원인 | AI 해결 방식 |
|---|---|
| 타이밍 문제 | 적절한 대기 조건 자동 삽입 (waitForSelector, networkidle) |
| 데이터 의존성 | 테스트 간 데이터 격리 전략 자동 적용 |
| 애니메이션 간섭 | 애니메이션 완료 대기 또는 비활성화 코드 삽입 |
| 렌더링 순서 | toBeVisible() 같은 안정적인 단언으로 자동 전환 |
| 네트워크 불안정 | 재시도 로직 및 모킹 전략 자동 적용 |
import { Page } from "@playwright/test";
/**
* 페이지 안정화 유틸리티
* 네트워크 요청 완료와 애니메이션 종료를 기다립니다.
*/
export async function waitForStable(page: Page): Promise<void> {
// 네트워크 유휴 상태 대기
await page.waitForLoadState("networkidle");
// 진행 중인 애니메이션 완료 대기
await page.evaluate(() => {
return Promise.all(
document.getAnimations().map((animation) => animation.finished)
);
});
}
/**
* 재시도 가능한 액션 실행
* 일시적인 실패에 대해 지정된 횟수만큼 재시도합니다.
*/
export async function retryAction<T>(
action: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 500
): Promise<T> {
let lastError: Error | undefined;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await action();
} catch (error) {
lastError = error as Error;
if (attempt < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
}
throw lastError;
}Playwright는 내장된 자동 대기(auto-waiting) 메커니즘이 있어 대부분의 타이밍 문제를 해결합니다. page.click()은 요소가 표시되고, 활성화되고, 안정적인 상태가 될 때까지 자동으로 기다립니다. AI 도구는 이러한 내장 기능을 최대한 활용하도록 테스트를 생성합니다.
이 장에서는 AI 기반 E2E 테스트 자동화의 현재를 살펴보았습니다.
핵심 내용을 정리하면 다음과 같습니다.
5장에서는 변이 테스트(Mutation Testing)를 다룹니다. AI가 생성한 테스트가 정말 결함을 잡아낼 수 있는지를 검증하는 방법으로, 코드에 의도적인 변이를 주입하여 테스트 스위트의 실질적 품질을 측정합니다. Stryker, PIT, mutmut 같은 도구를 활용한 실습도 진행합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
변이 테스트의 원리와 변이 연산자를 이해하고, Stryker, PIT, mutmut 도구로 AI 생성 테스트의 품질을 검증하는 방법을 다룹니다. 변이 점수 측정과 비용-효과 분석도 포함합니다.
API 스키마 기반 테스트 자동 생성, 계약 테스트(Contract Testing), testcontainers와 AI를 결합한 데이터베이스 통합 테스트, 그리고 CI 파이프라인 통합 방법을 다룹니다.
픽셀 비교의 한계를 넘어 Visual AI 기반 시각적 회귀 테스트를 다룹니다. Applitools Eyes, Percy, Chromatic 비교 분석과 동적 콘텐츠 처리, 반응형 레이아웃 테스트, 스토리북 통합을 안내합니다.