E2E 테스트 구현
단위 테스트와 통합 테스트는 애플리케이션의 개별 기능과 모듈 간의 상호작용을 검증하는 데 중요합니다. 하지만 실제 사용자가 브라우저에서 애플리케이션과 상호작용하는 모든 과정을 처음부터 끝까지 시뮬레이션하고 검증하는 것은 어렵습니다. 이때 필요한 것이 E2E (End-to-End) 테스트입니다. E2E 테스트는 애플리케이션이 실제 프로덕션 환경에서 예상대로 작동하는지 최종적으로 확인하는 데 사용됩니다.
이 절에서는 E2E 테스트의 개념과 중요성, Next.js 프로젝트에서 E2E 테스트를 위한 강력한 도구인 Cypress를 설정하고 사용하는 방법, 그리고 실제 사용자 시나리오를 기반으로 한 테스트를 작성하는 방법에 대해 상세히 알아보겠습니다.
E2E 테스트란 무엇이며 왜 중요한가요?
E2E 테스트는 사용자의 관점에서 애플리케이션의 전체 흐름을 테스트하는 방식입니다. 이는 사용자가 웹 브라우저를 열고, 로그인하고, 특정 페이지로 이동하고, 데이터를 입력하고, 버튼을 클릭하는 등의 일련의 상호작용을 자동화된 방식으로 시뮬레이션합니다. E2E 테스트는 백엔드 API, 데이터베이스, 네트워크 등 애플리케이션의 모든 계층을 포함하여 실제 운영 환경과 가장 유사한 조건에서 시스템 전체의 동작을 검증합니다.
E2E 테스트의 중요성
- 실제 사용자 경험 검증: 사용자 관점에서 핵심 비즈니스 로직과 UI 상호작용이 올바르게 작동하는지 확인합니다.
- 통합된 시스템 검증: 프론트엔드, 백엔드, 데이터베이스, 네트워크 등 모든 구성 요소가 함께 잘 작동하는지 확인하여 전체 시스템의 견고함을 보장합니다.
- 배포 전 최종 안전망: 프로덕션 환경에 배포하기 전에 발생할 수 있는 가장 치명적인 문제를 발견하고 수정할 수 있는 마지막 기회를 제공합니다.
- 신뢰도 향상: 복잡한 사용자 여정을 테스트함으로써, 애플리케이션에 대한 개발 팀과 이해 관계자들의 신뢰도를 높입니다.
- 회귀(Regression) 방지: 새로운 기능 추가나 코드 변경이 기존 기능에 예상치 못한 부작용을 일으키는 것을 방지합니다.
Cypress란?
Cypress는 현대적인 웹 애플리케이션을 위한 빠르고 안정적인 E2E 테스트 프레임워크입니다. Selenium과 같은 기존 도구와 달리, Cypress는 브라우저 내부에서 직접 실행되므로 더 빠르고 안정적인 테스트 경험을 제공합니다. 개발자 친화적인 API, 실시간 리로드, 디버깅 기능, 비디오 녹화 등 다양한 기능을 내장하고 있습니다.
Cypress의 주요 특징
- 빠른 실행: 브라우저와 동일한 실행 루프에서 테스트가 실행되어 빠르고 신뢰할 수 있습니다.
- 쉬운 디버깅: 개발자 도구에 직접 접근할 수 있어 테스트 실행 중 문제를 쉽게 디버깅할 수 있습니다.
- 자동 재실행: 파일 변경 시 테스트가 자동으로 재실행되어 개발 흐름을 방해하지 않습니다.
- 스크린샷 및 비디오: 테스트 실패 시 자동으로 스크린샷을 찍고, 전체 테스트 과정을 비디오로 녹화하여 디버깅을 돕습니다.
- 친숙한 API: jQuery와 유사한 명령어를 사용하여 DOM 조작 및 이벤트 트리거가 직관적입니다.
Next.js 프로젝트에 Cypress 설정하기
Next.js는 Cypress를 위한 통합된 설정을 제공하며, 설치 및 초기 설정이 매우 간단합니다.
Cypress 설치
프로젝트 루트 디렉토리에서 Cypress를 개발 의존성으로 설치합니다.
npm install --save-dev cypress
# 또는
yarn add --dev cypress
package.json
에 스크립트 추가
package.json
파일의 scripts
섹션에 Cypress 실행 명령어를 추가합니다.
// package.json
{
"name": "your-nextjs-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "jest",
"test:watch": "jest --watch",
"cypress:open": "cypress open", // Cypress UI를 열어 테스트를 실행하고 디버깅
"cypress:run": "cypress run" // 헤드리스 모드로 모든 테스트 실행 (CI/CD용)
},
// ... (dependencies, devDependencies)
}
Cypress 초기 설정 및 구조
npm run cypress:open
또는 yarn cypress:open
명령어를 처음 실행하면, Cypress가 필요한 파일과 디렉토리를 자동으로 생성합니다.
cypress/
디렉토리 생성: 모든 Cypress 관련 파일이 이 디렉토리 내에 생성됩니다.cypress/e2e/
: E2E 테스트 스펙 파일이 위치합니다.cypress/support/e2e.ts
(또는e2e.js
): 모든 테스트 파일에 앞서 실행되는 전역 설정 파일입니다. 커스텀 명령어나 공통적으로 필요한 설정을 여기에 추가합니다.cypress/support/commands.ts
: 커스텀 Cypress 명령어를 정의합니다.cypress/fixtures/
: 테스트에 필요한 정적 데이터를 저장합니다.cypress.config.ts
(또는cypress.config.js
): Cypress의 메인 설정 파일입니다.
cypress.config.ts
예시
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
// 테스트 대상이 되는 애플리케이션의 기본 URL (Next.js 개발 서버 URL)
baseUrl: 'http://localhost:3000',
// 테스트 스펙 파일의 경로 패턴
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
// 테스트 시작 전 실행될 setup 파일
setupNodeEvents(on, config) {
// Node.js 이벤트 리스너를 설정하여 Cypress가 Node 환경과 상호작용하도록 합니다.
// 예: 파일 시스템 접근, 환경 변수 로드 등
},
// 비디오 녹화 활성화
video: true,
// 스크린샷 활성화 (테스트 실패 시 자동으로 촬영)
screenshotOnRunFailure: true,
},
// 컴포넌트 테스트 설정 (선택 사항)
component: {
devServer: {
framework: 'next',
bundler: 'webpack',
},
specPattern: 'components/**/*.cy.{js,jsx,ts,tsx}',
},
});
Next.js 개발 서버 실행
Cypress는 실제 브라우저에서 동작하므로, 테스트를 실행하기 전에 Next.js 개발 서버가 실행 중이어야 합니다.
npm run dev
# 또는
yarn dev
별도의 터미널에서 이 명령어를 실행한 후 Cypress 테스트를 실행합니다.
기본적인 E2E 테스트 작성하기
사용자 로그인 시나리오를 예시로 E2E 테스트를 작성해 보겠습니다.
시나리오
- 로그인 페이지로 이동한다.
- 사용자 이름과 비밀번호를 입력한다.
- 로그인 버튼을 클릭한다.
- 로그인 성공 시 대시보드 페이지로 리다이렉트 되는 것을 확인한다.
- 대시보드 페이지에 사용자 이름이 표시되는 것을 확인한다.
사전 준비 (가상의 로그인 페이지 및 대시보드 페이지)
// app/login/page.tsx
"use client";
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// 가상 로그인 API 호출 (실제로는 백엔드 API를 호출)
if (username === 'testuser' && password === 'password123') {
alert('로그인 성공!');
router.push('/dashboard');
} else {
alert('로그인 실패: 사용자 이름 또는 비밀번호가 올바르지 않습니다.');
}
};
return (
<div style={{ maxWidth: '400px', margin: '50px auto', padding: '30px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<h1 style={{ textAlign: 'center', marginBottom: '30px', color: '#333' }}>로그인</h1>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '20px' }}>
<label htmlFor="username" style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>사용자 이름:</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
</div>
<div style={{ marginBottom: '30px' }}>
<label htmlFor="password" style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>비밀번호:</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ width: '100%', padding: '10px', borderRadius: '4px', border: '1px solid #ccc' }}
/>
</div>
<button
type="submit"
style={{ width: '100%', padding: '12px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '1.1em' }}
>
로그인
</button>
</form>
</div>
);
}
// app/dashboard/page.tsx
"use client";
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function DashboardPage() {
const router = useRouter();
const [user, setUser] = useState('테스트 사용자'); // 실제로는 로그인 상태 관리 시스템에서 가져옴
// 간단한 로그인 여부 확인 로직 (실제 앱에서는 더 복잡)
useEffect(() => {
// 실제로는 토큰 확인 등의 로직
// if (!isLoggedIn) {
// router.push('/login');
// }
}, [router]);
const handleLogout = () => {
alert('로그아웃 되었습니다.');
router.push('/login');
};
return (
<div style={{ maxWidth: '800px', margin: '50px auto', padding: '30px', border: '1px solid #28a745', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<h1 style={{ textAlign: 'center', marginBottom: '30px', color: '#28a745' }}>대시보드</h1>
<p style={{ fontSize: '1.2em', textAlign: 'center', marginBottom: '40px' }}>
환영합니다, <span style={{ fontWeight: 'bold', color: '#007bff' }}>{user}</span>님!
</p>
<div style={{ display: 'flex', justifyContent: 'center', gap: '20px' }}>
<button
onClick={() => alert('프로필 보기 클릭')}
style={{ padding: '12px 25px', backgroundColor: '#6c757d', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '1.1em' }}
>
프로필 보기
</button>
<button
onClick={handleLogout}
style={{ padding: '12px 25px', backgroundColor: '#dc3545', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', fontSize: '1.1em' }}
>
로그아웃
</button>
</div>
</div>
);
}
Cypress 테스트 파일 (cypress/e2e/login.cy.ts
)
// cypress/e2e/login.cy.ts
describe('로그인 기능 E2E 테스트', () => {
beforeEach(() => {
// 각 테스트가 시작되기 전에 로그인 페이지로 이동합니다.
cy.visit('/login');
});
it('올바른 자격 증명으로 로그인에 성공해야 합니다.', () => {
// 1. 사용자 이름 입력 필드를 찾고 값을 입력합니다.
cy.get('#username').type('testuser');
// 2. 비밀번호 입력 필드를 찾고 값을 입력합니다.
cy.get('#password').type('password123');
// 3. 로그인 버튼을 찾고 클릭합니다.
cy.get('button[type="submit"]').click();
// 4. 로그인 성공 알림이 뜨고, 대시보드 페이지로 리다이렉트 되는 것을 확인합니다.
// window.alert를 모킹하여 Cypress가 알림창을 처리하도록 합니다.
cy.on('window:alert', (str) => {
expect(str).to.equal('로그인 성공!');
});
// URL이 '/dashboard'로 변경되었는지 확인합니다.
cy.url().should('include', '/dashboard');
// 5. 대시보드 페이지에 "환영합니다, 테스트 사용자님!" 텍스트가 표시되는 것을 확인합니다.
cy.contains('환영합니다, 테스트 사용자님!').should('be.visible');
});
it('잘못된 자격 증명으로 로그인에 실패해야 합니다.', () => {
// 잘못된 사용자 이름과 비밀번호 입력
cy.get('#username').type('wronguser');
cy.get('#password').type('wrongpassword');
// 로그인 버튼 클릭
cy.get('button[type="submit"]').click();
// 로그인 실패 알림이 뜨는 것을 확인합니다.
cy.on('window:alert', (str) => {
expect(str).to.equal('로그인 실패: 사용자 이름 또는 비밀번호가 올바르지 않습니다.');
});
// 여전히 로그인 페이지에 머물러 있는지 확인합니다.
cy.url().should('include', '/login');
cy.contains('로그인').should('be.visible');
});
it('로그인 후 로그아웃 기능이 작동해야 합니다.', () => {
// 먼저 로그인에 성공합니다.
cy.get('#username').type('testuser');
cy.get('#password').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('환영합니다, 테스트 사용자님!').should('be.visible');
// 로그아웃 버튼을 클릭합니다.
cy.get('button').contains('로그아웃').click();
// 로그아웃 알림이 뜨고, 로그인 페이지로 리다이렉트 되는 것을 확인합니다.
cy.on('window:alert', (str) => {
expect(str).to.equal('로그아웃 되었습니다.');
});
cy.url().should('include', '/login');
cy.contains('로그인').should('be.visible');
});
});
테스트 실행
- Next.js 개발 서버를 실행합니다:
npm run dev
- Cypress 테스트 러너를 엽니다:
npm run cypress:open
- Cypress UI에서
e2e
테스트를 선택하고, 방금 생성한login.cy.ts
파일을 클릭하여 테스트를 실행합니다.
Cypress가 브라우저를 열고, 정의된 시나리오에 따라 자동으로 상호작용하며 테스트 결과를 보여줄 것입니다.
Cypress 활용 팁 및 고급 기능
- 셀렉터 사용
- Cypress는
cy.get()
을 사용하여 DOM 요소를 선택합니다.data-testid
속성과 같이 테스트 전용 속성을 사용하는 것이 가장 안정적입니다. cy.get('[data-testid="login-button"]').click();
- 텍스트로 찾기:
cy.contains('로그인')
- Cypress는
- 비동기 처리: Cypress 명령어는 기본적으로 비동기적으로 작동하며, 자동으로 재시도 로직을 포함합니다.
cy.get().should('be.visible')
와 같이 어설션 체이닝을 통해 요소가 나타날 때까지 기다릴 수 있습니다. - 네트워크 요청 모킹 (cy.intercept)
cy.intercept()
를 사용하여 백엔드 API 요청을 가로채고 Mock 응답을 반환할 수 있습니다. 이는 테스트의 속도를 높이고, 외부 API의 영향을 받지 않는 안정적인 테스트를 가능하게 합니다.cy.intercept('POST', '/api/login', { statusCode: 200, body: { message: '로그인 성공', user: { name: 'testuser' } }, }).as('loginRequest'); // 요청에 별칭 부여 // ... 로그인 시도 ... cy.wait('@loginRequest').its('response.statusCode').should('eq', 200); // 요청 대기 및 검증
- 커스텀 명령어: 자주 사용되는 일련의 동작(예: 로그인)을
cypress/support/commands.ts
에 커스텀 명령어로 정의하여 재사용성을 높일 수 있습니다.cypress/support/commands.ts // cypress/support/commands.ts Cypress.Commands.add('login', (username, password) => { cy.visit('/login'); cy.get('#username').type(username); cy.get('#password').type(password); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); }); // 사용: cy.login('testuser', 'password123');
- CI/CD 통합:
cypress run
명령어를 사용하여 헤드리스 모드로 Cypress 테스트를 실행할 수 있습니다. 이는 CI/CD 파이프라인에 통합하여 코드 푸시 또는 배포 전에 자동으로 E2E 테스트를 실행하는 데 사용됩니다. - 리셋 상태: 각 테스트는 독립적으로 실행되어야 합니다.
beforeEach
훅을 사용하여 테스트 환경(예: 로그인 상태, 로컬 스토리지)을 초기화하거나, 데이터베이스를 정리하는 등의 작업을 수행하는 것이 좋습니다.
E2E 테스트는 애플리케이션의 최종 품질을 보장하는 데 매우 중요합니다. Cypress는 Next.js 애플리케이션의 복잡한 사용자 흐름을 효과적으로 테스트할 수 있는 강력하고 직관적인 도구를 제공하여, 개발자가 자신감을 가지고 배포할 수 있도록 돕습니다.