devops2025-02-15·9 min·198/348

Cypress E2E 테스트 설정: 엔드투엔드 테스트 환경 구성

Cypress E2E 테스트 설정과 환경 구성 방법을 설명합니다.

Cypress E2E 테스트 설정

Introduction

Cypress는 현대적인 웹 애플리케이션을 위한 엔드투엔드(E2E) 테스트 프레임워크입니다. 브라우저에서 직접 실행되어 실제 사용자와 유사한 테스트 시나리오를 구현할 수 있습니다. 이 글에서는 Cypress 설정부터 최적화까지 전체 프로세스를 다루겠습니다.

Environment

// package.json
{
  "name": "my-project",
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run",
    "cypress:run:chrome": "cypress run --browser chrome",
    "test:e2e": "start-server-and-test dev http://localhost:3000 cypress:open",
    "test:e2e:ci": "start-server-and-test dev http://localhost:3000 cypress:run"
  },
  "devDependencies": {
    "cypress": "^13.6.0",
    "start-server-and-test": "^2.0.0"
  }
}
npx cypress --version
# Cypress 13.6.1

Problem

Cypress 설정 시 발생하는 문제들:

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}'
  }
});

// 문제점:
// 1. 브라우저 설정 미흡
// 2. 시간 초과 문제
// 3. 네트워크 요청 처리 미숙
# 테스트 실행 시 에러
npx cypress run

# 에러:
# CypressError: Timed out retrying after 5000ms:
# cy.visit() failed trying to load:
# http://localhost:3000
# We attempted to make an http request to this URL but the request failed without a response.

Analysis

Cypress 설정 구조를 분석했습니다:

// cypress.config.js 구조 분석
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  // 프로젝트 설정
  projectId: 'abc123',
  
  // 브라우저 설정
  chromeWebSecurity: true,
  
  // E2E 테스트 설정
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    supportFile: 'cypress/support/e2e.js',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    requestTimeout: 10000,
    responseTimeout: 30000,
    pageLoadTimeout: 60000
  },
  
  // 컴포넌트 테스트 설정
  component: {
    devServer: {
      framework: 'react',
      bundler: 'vite'
    }
  }
});
# Cypress 폴더 구조
cypress/
├── e2e/
│   ├── auth/
│   │   ├── login.cy.ts
│   │   └── register.cy.ts
│   └── dashboard.cy.ts
├── fixtures/
│   └── users.json
├── support/
│   ├── commands.ts
│   └── e2e.ts
└── tsconfig.json

Solution

1단계: 기본 설정 구성

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  projectId: 'abc123',
  
  chromeWebSecurity: false,
  
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    supportFile: 'cypress/support/e2e.ts',
    
    // 뷰포트 설정
    viewportWidth: 1280,
    viewportHeight: 720,
    
    // 타임아웃 설정
    defaultCommandTimeout: 10000,
    requestTimeout: 10000,
    responseTimeout: 30000,
    pageLoadTimeout: 60000,
    
    // 비디오 녹화
    video: true,
    videoCompression: 32,
    
    // 스크린샷
    screenshotOnRunFailure: true,
    trashAssetsBeforeRuns: true,
    
    // 로깅
    experimentalRunAllSpecs: true
  }
});

2단계: 커스텀 커맨드 작성

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable;
      getByTestId(testId: string): Chainable>;
      interceptGQL(operationName: string): Chainable;
    }
  }
}

Cypress.Commands.add('login', (email: string, password: string) => {
  cy.session([email, password], () => {
    cy.visit('/login');
    cy.get('[data-testid="email-input"]').type(email);
    cy.get('[data-testid="password-input"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
  });
});

Cypress.Commands.add('getByTestId', (testId: string) => {
  return cy.get(`[data-testid="${testId}"]`);
});

Cypress.Commands.add('interceptGQL', (operationName: string) => {
  cy.intercept('POST', '/graphql', (req) => {
    if (req.body.operationName === operationName) {
      req.alias = operationName;
    }
  });
});

// cypress/support/e2e.ts
import './commands';

// 전역 에러 핸들링
Cypress.on('uncaught:exception', (err, runnable) => {
  return false;
});

3단계: 테스트 시나리오 작성

// cypress/e2e/auth/login.cy.ts
describe('Login', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('should login successfully', () => {
    cy.get('[data-testid="email-input"]').type('user@example.com');
    cy.get('[data-testid="password-input"]').type('password123');
    cy.get('[data-testid="login-button"]').click();
    
    cy.url().should('include', '/dashboard');
    cy.get('[data-testid="welcome-message"]').should('contain', 'Welcome');
  });

  it('should show error for invalid credentials', () => {
    cy.get('[data-testid="email-input"]').type('wrong@example.com');
    cy.get('[data-testid="password-input"]').type('wrongpassword');
    cy.get('[data-testid="login-button"]').click();
    
    cy.get('[data-testid="error-message"]').should('be.visible');
  });
});

// cypress/e2e/dashboard.cy.ts
describe('Dashboard', () => {
  beforeEach(() => {
    cy.login('user@example.com', 'password123');
    cy.visit('/dashboard');
  });

  it('should display user data', () => {
    cy.interceptGQL('GetUserData');
    cy.wait('@GetUserData');
    
    cy.get('[data-testid="user-stats"]').should('be.visible');
  });
});

Lessons Learned

  1. data-testid 활용: 테스트 안정성을 위해 CSS 클래스 대신 data-testid 속성을 사용해야 합니다
  2. cy.session 활용: 로그인 세션을 캐싱하여 테스트 실행 시간을 단축할 수 있습니다
  3. 네트워크 인터셉트: API 요청을 인터셉트하여 테스트 환경을 격리해야 합니다
  4. 타임아웃 조정: 테스트 안정성을 위해 적절한 타임아웃 값을 설정해야 합니다
  5. CI/CD 통합: Cypress Dashboard와 연동하여 테스트 결과를 모니터링하는 것이 효과적입니다

이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.