테스트 관련 강좌

테스트 케이스 몇 장으로는 절대 못 잡는 것들, SauceDemo로 배우는 진짜 테스트 자동화 설계

testmanager 2026. 5. 6. 06:06
반응형

단순히 "로그인 되나요?"를 확인하는 것과, 그 로그인이 언제 어떤 조건에서 무너지는지를 미리 아는 것 사이엔 생각보다 큰 간격이 있다.

 

SauceDemo라는 작은 쇼핑몰 사이트가 그 간격을 체감하기에 이보다 좋은 실험장은 없다.

 

2026.05.06 - [분류 전체보기] - [공유] 소프트웨어 테스트 대상 사이트/앱

 

[공유] 소프트웨어 테스트 대상 사이트/앱

실습할수있는 공개 웹사이트/앱 난이도 순 플랫폼추천 Tier난이도주요 강점AI 자동화 핵심 포인트공개 APICI/CD연계결함예측UI 복잡도접근 방식SauceDemo1하E2E 이커머스 표준 + 결함 계정Self-Healing, 시

testmanager.tistory.com

 

SauceDemo (Swag Labs) — https://www.saucedemo.com/
- 이커머스 E2E 흐름(로그인→장바구니→결제), 의도적 결함 계정 다수 보유
- 시나리오 자동 생성, Self-Healing, Visual Testing, 결함 예측 최적

 

 


"어떤 테스트를 짤까"보다 먼저 해야것

 

"어떤 테스트를 짤까"보다 먼저 해야 할 것

 

테스트 자동화 프로젝트가 실패하는 이유를 돌아보면, 대부분 코드를 잘못 짜서가 아니다.

 

무엇을, 왜, 어떤 순서로 테스트해야 하는지에 대한 설계가 없는 채로 스크립트부터 작성하기 때문이다.

 

SauceDemo는 Sauce Labs가 공개한 데모 쇼핑몰이다.

 

겉보기엔 단순하지만, 안을 들여다보면 의도적으로 심어둔 결함 사용자(performance_glitch_user, error_user, problem_user)가 존재하고, 각 사용자 유형마다 시스템이 다르게 반응하도록 설계되어 있다.

 

이 구조는 테스트 설계자에게 굉장히 솔직한 질문을 던진다. "당신의 테스트는 이 결함을 진짜로 잡을 수 있습니까?"

 

여기서 출발점은 코드가 아니라 요구사항이다.


 

기능 명세가 없어도 요구사항은 추출할 수 있다

 

SauceDemo에 공식 기능 명세서는 없다.

 

하지만 실제 프로젝트에서도 문서가 항상 완비된 경우는 드물다.

 

진짜 역량은 애플리케이션 자체로부터 요구사항을 역추출하는 데 있다.

 

사이트를 기능 도메인별로 분해하면 인증(Authentication), 상품 목록(Product Catalog), 장바구니(Cart), 주문 흐름(Checkout Flow), 그리고 네비게이션과 정렬·필터링이 핵심 영역으로 드러난다.

 

각 도메인은 독립적인 것처럼 보이지만 실제로는 강하게 결합되어 있다.

 

로그인 세션 상태가 장바구니에 영향을 주고, 장바구니 수량이 checkout 흐름 전체의 전제가 된다.

 

이 결합 관계를 도식화하는 것이 테스트 아키텍처의 첫 번째 작업이다.

 

요구사항 분석 단계에서 특히 집중해야 하는 지점은 "경계값"과 "예외 흐름"이다.

 

예를 들어, 장바구니에 상품이 0개인 상태에서 checkout 버튼이 어떻게 반응해야 하는가, 혹은 locked_out_user로 로그인 시도 시 에러 메시지의 문구와 UX 처리 방식이 명세와 일치하는가.

 

이런 질문들이 요구사항 분석의 실질적인 산출물이 된다.


테스트 피라미드를 실제로 세운다는 것의 의미

 

"Unit → API → E2E"라는 테스트 피라미드는 업계에서 워낙 자주 언급되는 탓에 오히려 형식적으로 흘러가는 경향이 있다.

 

SauceDemo를 대상으로 이 피라미드를 구체적으로 설계하면 각 레이어의 역할이 훨씬 선명해진다.

 

Unit 테스트 레이어에서는 비즈니스 로직의 순수 검증에 집중한다.

 

상품 가격 계산, 세금 산출(Tax = 소계 × 0.08), 정렬 알고리즘(이름 오름차순/내림차순, 가격 기준)이 대표적인 대상이다.

 

이 로직들은 UI와 독립적으로 검증되어야 하며, Jest나 JUnit 같은 단위 테스트 프레임워크에서 밀리초 단위로 실행되어야 한다.

 

API 레이어는 SauceDemo가 REST API를 공개하지 않아 조금 다른 접근이 필요하다.

 

실제 프로젝트에서의 대응 전략은 두 가지다.

 

하나는 브라우저 네트워크 탭을 분석해 내부 XHR/Fetch 요청을 식별하는 것이고, 다른 하나는 Mock Server(MSW, WireMock 등)를 활용해 API 계층을 시뮬레이션하는 것이다.

 

후자는 특히 error_user 시나리오처럼 서버 에러 응답을 의도적으로 주입해야 하는 경우에 강력한 도구가 된다.

 

E2E 레이어는 Playwright 또는 Cypress를 중심으로 구성하되, 핵심은 페이지 오브젝트 모델(POM) 패턴을 엄격하게 적용하는 데 있다.

 

UI 선택자가 바뀔 때마다 수십 개의 테스트 파일을 수정해야 하는 상황은 유지보수 비용을 급격히 증가시킨다.

 

POM 구조에서는 LoginPage, InventoryPage, CartPage, CheckoutPage 각각이 독립적인 클래스로 분리되고, 선택자 변경은 해당 클래스 내부에서만 처리된다.


problem_user가 폭로하는 것들, 결함 예측 기반 설계

 

SauceDemo의 problem_user 계정은 단순히 "버그가 있는 사용자"가 아니다.

 

이 계정은 상품 이미지가 잘못 렌더링되고, 일부 상품의 장바구니 추가가 실패하며, 정렬 기능이 의도치 않게 작동한다.

 

테스트 설계 관점에서 이것은 결함 클러스터링(Defect Clustering)의 교과서적 사례다.

 

결함은 특정 모듈이나 기능 경계에 집중되는 경향이 있다.

 

이 원리를 테스트 설계에 적용하면, 모든 기능을 동등하게 테스트하는 것보다 리스크가 높은 영역에 테스트 자원을 집중 배분하는 리스크 기반 테스트(Risk-Based Testing) 전략이 훨씬 효율적임을 알 수 있다.

 

구체적으로는 각 기능 영역에 대해 결함 발생 가능성(Likelihood)과 비즈니스 영향도(Impact)를 2차원 매트릭스로 평가한다.

 

Checkout 흐름의 결제 완료 단계는 발생 가능성이 낮더라도 영향도가 극히 높기 때문에 최우선 테스트 대상이 된다.

 

반면 상품 정렬 기능의 시각적 순서는 상대적으로 낮은 우선순위에 배치된다.

 

이 매트릭스가 테스트 케이스 수와 실행 빈도를 결정하는 근거가 된다.


Playwright로 작성한 테스트, 어떻게 읽히는지가 중요하다

 

자동화 스크립트의 품질은 실행 결과뿐 아니라 코드의 가독성과 유지보수성으로도 평가된다.

 

실제로 테스트 코드를 읽지 못하면 실패 원인을 파악하는 데 불필요한 시간이 소요되고, 결국 자동화를 포기하게 만드는 원인이 된다.

 

아래는 SauceDemo의 전체 주문 흐름을 검증하는 E2E 테스트의 핵심 구조다.

 

Playwright 기준으로 작성했다.

// pages/LoginPage.js
class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.locator('[data-test="username"]');
    this.passwordInput = page.locator('[data-test="password"]');
    this.loginButton = page.locator('[data-test="login-button"]');
    this.errorMessage = page.locator('[data-test="error"]');
  }

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async getErrorMessage() {
    return this.errorMessage.textContent();
  }
}

// pages/CheckoutPage.js
class CheckoutPage {
  constructor(page) {
    this.page = page;
    this.firstNameInput = page.locator('[data-test="firstName"]');
    this.lastNameInput = page.locator('[data-test="lastName"]');
    this.postalCodeInput = page.locator('[data-test="postalCode"]');
    this.continueButton = page.locator('[data-test="continue"]');
    this.finishButton = page.locator('[data-test="finish"]');
    this.successHeader = page.locator('.complete-header');
    this.totalPrice = page.locator('.summary_total_label');
  }

  async fillShippingInfo(firstName, lastName, postalCode) {
    await this.firstNameInput.fill(firstName);
    await this.lastNameInput.fill(lastName);
    await this.postalCodeInput.fill(postalCode);
    await this.continueButton.click();
  }

  async getTotalPrice() {
    const text = await this.totalPrice.textContent();
    return parseFloat(text.replace('Total: $', ''));
  }
}

// tests/checkout.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';
import { CartPage } from '../pages/CartPage';
import { CheckoutPage } from '../pages/CheckoutPage';

test.describe('전체 구매 흐름 검증', () => {
  test('표준 사용자의 단일 상품 주문 완료 시나리오', async ({ page }) => {
    const login = new LoginPage(page);
    const inventory = new InventoryPage(page);
    const cart = new CartPage(page);
    const checkout = new CheckoutPage(page);

    await page.goto('https://www.saucedemo.com/');
    await login.login('standard_user', 'secret_sauce');
    await expect(page).toHaveURL(/inventory/);

    await inventory.addItemToCart('Sauce Labs Backpack');
    await inventory.goToCart();

    await cart.proceedToCheckout();
    await checkout.fillShippingInfo('Gildong', 'Hong', '12345');

    // 가격 검증: 소계 + 세금(8%) = 총액
    const total = await checkout.getTotalPrice();
    expect(total).toBeCloseTo(32.39, 2); // $29.99 * 1.08

    await checkout.finishButton.click();
    await expect(checkout.successHeader).toHaveText('Thank you for your order!');
  });

  test('locked_out_user 접근 차단 및 에러 메시지 노출', async ({ page }) => {
    const login = new LoginPage(page);
    await page.goto('https://www.saucedemo.com/');
    await login.login('locked_out_user', 'secret_sauce');
    const error = await login.getErrorMessage();
    expect(error).toContain('Epic sadface: Sorry, this user has been locked out.');
  });
});

 

선택자를 data-test 속성 기반으로 통일한 점이 핵심이다.

 

CSS 클래스명이나 XPath는 UI 리팩토링 한 번에 전부 깨지지만, data-test 속성은 개발팀과의 약속으로 보호된다.

 

이 컨벤션을 도입하는 것만으로도 테스트 유지보수 비용이 체감상 절반 이하로 줄어드는 경험을 한다.

 


데이터 의존성을 제거하면 테스트가 스스로 달린다

 

자동화 테스트가 특정 데이터 상태에 종속되면 실행 순서가 중요해지고, 병렬 실행이 불가능해진다.

 

SauceDemo처럼 상태가 고정된 환경에서는 덜 두드러지지만, 실제 프로젝트에서는 테스트 간 데이터 간섭이 자동화 실패의 주범이 된다.

 

이 문제의 해결책은 각 테스트가 자신의 전제 데이터를 스스로 생성하고, 종료 후 정리하는 Test Fixture 패턴이다.

 

Playwright에서는 test.beforeEach와 test.afterEach를 활용하며, 특히 로그인 상태 유지를 위한 storageState 옵션을 사용하면 매 테스트마다 반복되는 로그인 과정을 건너뛰어 실행 속도를 크게 단축할 수 있다.

 

// playwright.config.js 일부
export default defineConfig({
  globalSetup: './global-setup.js', // 로그인 세션 사전 생성
  use: {
    storageState: 'playwright/.auth/user.json', // 세션 재사용
    baseURL: 'https://www.saucedemo.com',
    trace: 'on-first-retry', // 실패 시 자동 트레이스 수집
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  workers: 4, // 병렬 실행
});

 

trace: 'on-first-retry' 설정은 작지만 중요한 선택이다.

 

CI 환경에서 테스트가 실패했을 때, 로컬 재현 없이 Playwright의 트레이스 뷰어만으로 실패 시점의 DOM 상태, 네트워크 요청, 스크린샷 시퀀스를 전부 확인할 수 있다.

 

이것은 디버깅 시간을 실질적으로 줄여주는 기능이다.


GitHub Actions 안에서 테스트가 스스로 판단하게 만드는 방법

 

자동화 테스트가 CI/CD 파이프라인에 연결되지 않으면, 그것은 여전히 수동으로 실행해야 하는 반자동화일 뿐이다.

 

SauceDemo 기반의 테스트 스위트를 GitHub Actions에 연결하는 구성은 다음과 같다.

# .github/workflows/e2e-tests.yml
name: E2E Test Suite

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # 매일 오전 2시 스모크 테스트

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        browser: [chromium, firefox, webkit]  # 크로스 브라우저 병렬 실행
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Node.js 설정
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: 의존성 설치
        run: npm ci
      
      - name: Playwright 브라우저 설치
        run: npx playwright install --with-deps ${{ matrix.browser }}
      
      - name: E2E 테스트 실행
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          TEST_ENV: production
      
      - name: 테스트 리포트 업로드
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 30

 

schedule 트리거에 주목할 필요가 있다.

 

PR 시점에만 테스트를 돌리면 프로덕션 환경의 지속적인 상태 변화를 감지하지 못한다.

 

새벽 2시의 스케줄 실행은 외부 의존성이나 환경 변화로 인한 회귀를 조기에 탐지하는 역할을 한다.

 

이 한 줄이 "어제까지 됐는데 오늘 갑자기 안 돼요"라는 상황을 상당 부분 예방한다.


숫자로 말하는 테스트, 커버리지와 품질 지표를 어떻게 읽을 것인가

 

테스트 자동화의 성과를 설명할 때 "테스트 케이스 몇 개를 만들었다"는 무의미한 지표다.

 

실제로 의미 있는 수치는 다음과 같다.

 

코드 커버리지는 Unit 테스트 기준으로 핵심 비즈니스 로직(가격 계산, 검증 로직)에서 90% 이상을 목표로 한다.

 

단, 전체 커버리지 숫자가 높다고 품질이 좋은 것은 아니다.

 

커버리지는 "테스트가 실행된 코드의 비율"이지 "올바르게 검증된 코드의 비율"이 아니기 때문이다.

 

더 의미 있는 지표는 결함 탈출률(Defect Escape Rate)이다. 자동화 도입 전 프로덕션으로 탈출한 결함의 비율과 도입 후의 비율을 비교하면, 테스트 자동화가 실제로 얼마나 결함을 차단했는지가 드러난다.

 

SauceDemo 수준의 커버리지를 갖춘 테스트 스위트에서는 Login, Cart, Checkout 핵심 흐름의 회귀 결함 탈출률을 사실상 0에 가깝게 유지할 수 있다.

 

테스트 실행 시간도 추적해야 한다.

 

전체 E2E 스위트가 15분을 넘어가면 개발자들이 PR을 올리고 결과를 기다리는 과정에서 컨텍스트 스위칭이 발생한다.

 

4개 브라우저 병렬 실행과 storageState 세션 재사용을 결합하면 SauceDemo 기준 전체 스위트를 3~4분 내에 완료할 수 있으며, 이 수준이 개발 흐름을 방해하지 않는 임계점이다.

 

특히 인상적인 효과는 크로스 브라우저 회귀 탐지다.

 

problem_user가 WebKit 브라우저에서만 특정 방식으로 렌더링 오류를 일으키는 시나리오는 단일 브라우저 테스트로는 절대 잡히지 않는다.

 

GitHub Actions의 matrix 전략이 이 비용을 추가 작업 없이 처리해준다.


지금 이 설계가 다음 프로젝트에서도 유효한 이유

 

SauceDemo는 학습용 환경이지만, 여기서 구축한 아키텍처는 실제 프로젝트에서도 그대로 작동한다.

 

POM 패턴, 리스크 기반 우선순위 설계, CI/CD 연동, 병렬 실행 구성, 결함 예측 매트릭스—이 요소들은 대상 시스템이 바뀌어도 구조는 유지된다.

 

무엇보다 중요한 변화는 팀의 사고방식이다.

 

"이 기능이 돌아가나요?"를 묻는 것에서 "이 기능이 어떤 조건에서 무너지나요?"를 먼저 묻는 쪽으로 전환될 때, 테스트는 비로소 개발 주기의 후반 검수 단계가 아니라 설계 단계부터 품질을 형성하는 역할을 맡게 된다.

 

그리고 그 전환은, 대단한 도구가 아니라 올바른 설계에서 시작된다.


 

반응형