테스트 10분 읽기
프론트엔드 테스트 코드
프론트엔드 테스트 코드는 일반적으로 다음 네 가지로 구분할 수 있으며, 각각은 계층적으로 역할이 다르고, 목적도 명확히 구분됩니다.
프론트엔드 테스트 코드는 일반적으로 다음 네 가지로 구분할 수 있으며, 각각은 계층적으로 역할이 다르고, 목적도 명확히 구분됩니다.
✅ 프론트엔드 테스트 종류 요약
| 테스트 종류 | 대상 범위 | 속도 | 목적 | 주 사용 시점 |
|---|---|---|---|---|
| 유닛 테스트 | 함수, 컴포넌트 단위 | 빠름 | 개별 기능의 정확성 검증 | 함수 로직, 유틸, 훅 개발 시 |
| 통합 테스트 | 모듈 간 상호작용 | 보통 | 여러 컴포넌트/함수 조합 검증 | 커스텀 훅 + 컴포넌트 결합 시 |
| UI 테스트 | DOM 렌더링 + 이벤트 | 보통 | UI 요소 및 이벤트 반응 검증 | 폼, 버튼 클릭, 리스트 렌더링 등 |
| E2E 테스트 | 전체 사용자 흐름 | 느림 | 실제 유저 플로우 검증 | 기능 완성 후 릴리즈 전 단계 |
1️⃣ 유닛 테스트 (Unit Test)
📌 역할
- 작고 독립적인 유닛(함수, 클래스, 컴포넌트)이 주어진 입력에 대해 올바른 출력을 내는지 테스트.
- 예:
calculateTotalPrice(price: number, quantity: number): number
✅ 장점
- 빠르다 (ms 단위)
- 디버깅이 쉽고 실패 지점이 명확하다
- 로직에 대한 정확성 보장
❌ 단점
- UI나 사용자 상호작용 테스트는 어렵다
- 너무 작게 쪼개면 의미 없는 테스트가 생길 수 있음
💡 언제 사용?
- 유틸 함수, 커스텀 훅, 도메인 로직, form validation
🧪 적용 예시 (Jest + Testing Library or React Hooks Testing Library)
describe('calculateTotalPrice', () => {
it('should return correct total', () => {
expect(calculateTotalPrice(1000, 2)).toBe(2000);
});
});
2️⃣ 통합 테스트 (Integration Test)
📌 역할
- 여러 유닛이 함께 작동하는지 검증
- 예: 커스텀 훅 + 폼 컴포넌트 + API 요청
✅ 장점
- 유닛 간 결합이 잘 작동하는지 확인 가능
- 의존성 있는 코드의 동작 확인
❌ 단점
- 테스트 복잡성 증가
- 속도 느려질 수 있음
- 실패 원인 파악이 어려울 수 있음
💡 언제 사용?
- 컴포넌트와 훅을 함께 테스트할 때
- API 요청과 UI 연동을 검증할 때
🧪 적용 예시 (Jest + React Testing Library)
test('form submits with correct value', async () => {
render(<UserForm />);
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: 'Jinwoo' }
});
fireEvent.click(screen.getByRole('button'));
await waitFor(() => expect(api.submitUser).toHaveBeenCalledWith({ name: 'Jinwoo' }));
});
3️⃣ UI 테스트 (Component Testing / DOM Testing)
📌 역할
- UI 렌더링, 사용자 입력에 대한 반응, 버튼 클릭, 조건부 렌더링 등을 테스트
✅ 장점
- 실제 브라우저 환경에 가까운 테스트
- 사용자 경험을 코드로 보장 가능
❌ 단점
- 로직이 없으면 테스트가 너무 단순해질 수 있음
- 변경에 민감한 테스트가 많아질 수 있음 (fragile test)
💡 언제 사용?
- 사용자의 동작(입력, 클릭)이 발생하는 컴포넌트
- 조건부 렌더링 확인이 필요한 컴포넌트
🧪 적용 예시
test('button shows loading when clicked', () => {
render(<SubmitButton />);
fireEvent.click(screen.getByText('Submit'));
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
4️⃣ E2E 테스트 (End-to-End Test)
📌 역할
- 실제 브라우저에서 사용자의 전체 플로우를 테스트
- 예: 로그인 → 게시글 작성 → 게시글 목록 확인
✅ 장점
- 진짜 유저 경험과 거의 동일
- 통합 환경에서 문제를 조기에 발견 가능
❌ 단점
- 느리다 (초 단위)
- 환경 설정/유지 보수 복잡
- CI 환경에서 flaky test가 자주 발생
💡 언제 사용?
- 로그인 / 회원가입 / 결제 등 핵심 유저 플로우
- QA, 배포 전 회귀 테스트
🧪 적용 예시 (Cypress)
describe('Login Flow', () => {
it('logs in successfully', () => {
cy.visit('/login');
cy.get('input[name=email]').type('user@test.com');
cy.get('input[name=password]').type('secure123');
cy.get('button[type=submit]').click();
cy.url().should('include', '/dashboard');
});
});
🧠 정리
• 빠르고 안정적인 코드 품질 확보에는 유닛 + 통합 테스트가 핵심입니다.
• 사용자 경험 검증은 UI 테스트와 E2E 테스트로 커버합니다.
• 각 테스트는 중복이 아닌 협업 구조를 갖는 것이 좋습니다.
🌀 애자일 기반 테스트 구성 전략 개요
| 테스트 종류 | 애자일에서의 역할 | 적용 시점 | 적용 주체 | 자동화 여부 |
|---|---|---|---|---|
| 유닛 테스트 | 기능 단위 정확성 확보, 회귀 오류 방지 | 개발 중 | 개발자 | ✅ 자동화 |
| 통합 테스트 | 모듈 간 연결 테스트, 변화에 강한 아키텍처 유도 | 스프린트 중간 | 개발자 | ✅ 자동화 |
| UI 테스트 | UI 동작 보장, 변경된 요구사항의 반영 확인 | 스프린트 후반 또는 변경 시 | 개발자 + QA | ✅ 자동화 |
| E2E 테스트 | 사용자 플로우 보장, 릴리즈 전 기능 통합 확인 | 스프린트 종료 시점 | QA 또는 테스트 담당자 | ✅ 자동화(밤마다 CI) |
🪜 애자일 프로세스 단계별 테스트 적용 방식
1. 백로그 정리 & 요구사항 정의 (Sprint Planning)
- Acceptance Criteria를 바탕으로 테스트 시나리오 초안 작성
- 예: “사용자는 로그인 후 대시보드로 리다이렉트되어야 한다” → E2E 시나리오 정의
- 유닛/통합 테스트 가능한 기능과 그렇지 않은 기능 구분
2. 개발 시작 (Sprint Execution)
✅ 유닛 테스트 우선 작성 (TDD 지향)
- utils, form validation, 도메인 로직, 커스텀 훅 등
- React Testing Library 또는 Jest로 작성
// 예시: 이메일 유효성 검사 함수
test('isValidEmail returns true for valid email', () => {
expect(isValidEmail('user@example.com')).toBe(true);
});
✅ 통합 테스트 추가
- useForm + UI 컴포넌트 연결, API 요청 등
- 데이터 흐름, context 처리 검증
🛠 도구
- Jest + React Testing Library
- Mock Service Worker (MSW) 활용해 API mocking
3. UI 피드백 반영 & QA
- UI 이벤트/렌더링 테스트 작성
- aria-label, role, text 기반으로 사용자 경험 테스트
- Storybook 기반으로 비주얼 테스트 병행 가능
test('Clicking "Send" triggers submission', () => {
fireEvent.click(screen.getByRole('button', { name: 'Send' }));
expect(mockSubmit).toHaveBeenCalled();
});
4. 스프린트 종료 전 (Pre-Release)
- Cypress 또는 Playwright로 E2E 작성
- CI에 연결하여 릴리즈 전 자동 실행
- Critical Path만 우선 커버 → 점진적 확장
describe('End-to-End: Login Flow', () => {
it('logs in and shows dashboard', () => {
cy.visit('/login');
cy.get('input[name=email]').type('user@demo.com');
cy.get('input[name=password]').type('1234');
cy.get('button').click();
cy.url().should('include', '/dashboard');
});
});
🔄 CI/CD 파이프라인과 연계
예: GitHub Actions 기준
name: Frontend CI
on: [pull_request, push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- run: pnpm install
- run: pnpm test # 유닛 + 통합
- run: pnpm test:e2e # E2E (선택적)
🎯 요약
| 전략 | 설명 |
|---|---|
| ✅ TDD 시도 | 가능하면 유닛 테스트 기반의 개발 주도 |
| ✅ 스프린트 내 테스트 작성 | QA의 의존도를 줄이고, 반복 가능한 안정성 확보 |
| ✅ CI 자동화 | PR마다 테스트 실행 → 회귀 방지 |
| ✅ 실패 이유 빠른 파악 | 단위별 책임 분리 → 어느 테스트가 실패했는지 명확 |
| 🔁 테스트 리팩터링 주기적 실행 | 요구사항 변경에 따른 테스트 갱신 |
| ⚠️ E2E는 핵심 플로우만 우선 | 속도 및 유지보수 이슈 대비 |