테스트 전략 (Jest, RTL, Cypress)

2025. 4. 2. 15:29Frameworks/React

테스트 전략 (Jest, RTL, Cypress)

테스트가 왜 필요한가요?

테스트는 우리가 작성한 코드가 예상대로 작동하는지 자동으로 확인하는 방법입니다. 마치 숙제를 제출하기 전에 문제를 다시 풀어보는 것과 같습니다.

테스트의 장점

  • 버그 예방: 문제를 빨리 발견하고 수정할 수 있습니다
  • 리팩토링 자신감: 코드를 변경해도 기존 기능이 잘 작동하는지 확인할 수 있습니다
  • 문서화: 테스트는 코드가 어떻게 작동해야 하는지 보여주는 살아있는 문서입니다
  • 협업 용이: 다른 개발자들이 코드를 이해하고 수정하기 쉬워집니다

테스트 종류

  1. 단위 테스트 (Unit Test): 작은 기능 단위로 테스트합니다 (예: 함수, 컴포넌트)
  2. 통합 테스트 (Integration Test): 여러 기능이 함께 작동하는지 테스트합니다
  3. E2E 테스트 (End-to-End): 사용자 관점에서 전체 애플리케이션을 테스트합니다

Jest 소개

Jest는 Facebook에서 만든 JavaScript 테스트 프레임워크로, React 애플리케이션에서 가장 많이 사용됩니다.

Jest 설치하기

# create-react-app으로 만든 프로젝트에는 이미 포함되어 있습니다
npm install --save-dev jest

기본 Jest 테스트 예시

// sum.js
export function sum(a, b) {
  return a + b;
}

// sum.test.js
import { sum } from './sum';

test('1 + 2 = 3', () => {
  expect(sum(1, 2)).toBe(3);
});

Jest 테스트 실행하기

npm test

Jest 주요 매처(Matcher) 함수

// 동등 비교
expect(value).toBe(expectedValue); // 기본 타입 비교
expect(obj).toEqual(expectedObj); // 객체 비교

// 참/거짓 확인
expect(value).toBeTruthy();
expect(value).toBeFalsy();

// 배열/문자열 포함 여부
expect(array).toContain(item);
expect(string).toMatch(/regex/);

// 예외 발생 확인
expect(() => functionThatThrows()).toThrow();

Jest 모킹(Mocking) 기초

// 함수 모킹
const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();

// 반환값 설정
const mockFn = jest.fn().mockReturnValue('테스트');
console.log(mockFn()); // '테스트' 출력

// 모듈 모킹
jest.mock('./someModule', () => ({
  someFunction: jest.fn().mockReturnValue('목 반환값')
}));

React Testing Library (RTL) 소개

RTL은 React 컴포넌트를 테스트하기 위한 라이브러리로, 사용자 관점에서 컴포넌트를 테스트합니다.

RTL 설치하기

# create-react-app으로 만든 프로젝트에는 이미 포함되어 있습니다
npm install --save-dev @testing-library/react

기본 RTL 테스트 예시

// Button.jsx
function Button({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';

test('버튼 클릭 시 onClick 함수가 호출됨', () => {
  // 클릭 이벤트를 추적할 모의(mock) 함수 생성
  const handleClick = jest.fn();
  
  // 버튼 컴포넌트 렌더링
  render(<Button label="클릭하세요" onClick={handleClick} />);
  
  // 화면에서 버튼 찾기
  const button = screen.getByText('클릭하세요');
  
  // 버튼 클릭하기
  fireEvent.click(button);
  
  // onClick 함수가 호출되었는지 확인
  expect(handleClick).toHaveBeenCalledTimes(1);
});

컴포넌트 찾기 (Queries)

// 텍스트로 찾기
const element = screen.getByText('안녕하세요');

// 역할(role)로 찾기
const button = screen.getByRole('button', { name: '제출' });

// 레이블로 찾기
const input = screen.getByLabelText('이메일');

// 테스트 ID로 찾기
const element = screen.getByTestId('submit-button');

사용자 인터랙션 테스트

// 클릭 이벤트
fireEvent.click(button);

// 입력 이벤트
fireEvent.change(input, { target: { value: 'test@example.com' } });

// 키보드 이벤트
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });

// 사용자 이벤트 (더 실제와 가까운 방식)
import userEvent from '@testing-library/user-event';

test('사용자 입력 테스트', async () => {
  const user = userEvent.setup();
  
  render(<MyForm />);
  
  const nameInput = screen.getByLabelText('이름');
  await user.type(nameInput, '홍길동');
  
  const submitButton = screen.getByRole('button', { name: '제출' });
  await user.click(submitButton);
  
  // 결과 확인...
});

비동기 코드 테스트

test('데이터 로딩 후 표시', async () => {
  render(<UserProfile userId="1" />);
  
  // 로딩 상태 확인
  expect(screen.getByText('로딩 중...')).toBeInTheDocument();
  
  // 데이터가 로드될 때까지 대기
  const userName = await screen.findByText('홍길동');
  
  // 데이터 로드 후 확인
  expect(userName).toBeInTheDocument();
});

Cypress 소개

Cypress는 E2E 테스트 도구로, 실제 브라우저에서 애플리케이션을 테스트합니다.

Cypress 설치하기

npm install --save-dev cypress

Cypress 실행하기

npx cypress open

기본 Cypress 테스트 예시

// cypress/integration/login.spec.js
describe('로그인 페이지', () => {
  it('유효한 자격 증명으로 로그인 성공', () => {
    // 로그인 페이지 방문
    cy.visit('/login');
    
    // 사용자 이름과 비밀번호 입력
    cy.get('input[name="username"]').type('user123');
    cy.get('input[name="password"]').type('pass123');
    
    // 로그인 버튼 클릭
    cy.get('button[type="submit"]').click();
    
    // 로그인 성공 후 대시보드 페이지로 이동했는지 확인
    cy.url().should('include', '/dashboard');
    
    // 환영 메시지 확인
    cy.contains('환영합니다, user123님!');
  });
});

Cypress 주요 명령어

// 페이지 방문
cy.visit('/about');

// 요소 찾기
cy.get('.button').click();
cy.contains('제출').click();

// 입력
cy.get('input[name="email"]').type('test@example.com');

// 단언(Assertion)
cy.get('.message').should('be.visible');
cy.get('.count').should('have.text', '5');
cy.url().should('include', '/dashboard');

// 대기
cy.wait(1000); // 1초 대기
cy.get('.data').should('exist'); // 요소가 나타날 때까지 자동 대기

Cypress 고급 기능

// 네트워크 요청 가로채기
cy.intercept('GET', '/api/users', { fixture: 'users.json' });

// 쿠키 다루기
cy.getCookie('token');
cy.setCookie('token', 'abc123');

// 로컬 스토리지 다루기
cy.window().then((win) => {
  win.localStorage.setItem('token', 'abc123');
});

// 스크린샷 찍기
cy.screenshot('login-page');

// 커스텀 명령어 만들기
Cypress.Commands.add('login', (email, password) => {
  cy.visit('/login');
  cy.get('input[name="email"]').type(email);
  cy.get('input[name="password"]').type(password);
  cy.get('button[type="submit"]').click();
});

// 사용 예시
cy.login('test@example.com', 'password');

테스트 전략 수립하기

효과적인 테스트 전략을 수립하려면 다음과 같은 접근 방식을 고려해보세요:

테스트 피라미드 이해하기

  1. 단위 테스트 (80%): 많은 수의 빠른 테스트로 개별 함수와 컴포넌트 검증
  2. 통합 테스트 (15%): 중간 수준의 테스트로 컴포넌트 간 상호작용 검증
  3. E2E 테스트 (5%): 소수의 중요한 사용자 시나리오 검증

무엇을 테스트할지 정하기

  1. 핵심 비즈니스 로직: 중요한 계산, 데이터 변환, 유효성 검사 등
  2. 사용자 흐름: 로그인, 결제, 등록 등 중요한 사용자 경로
  3. 에지 케이스: 오류 상황, 경계값, 예외 처리 등

테스트 도구 선택 가이드

  1. Jest: 유틸리티 함수, 서비스, 커스텀 훅 등의 단위 테스트
  2. RTL: React 컴포넌트의 단위 및 통합 테스트
  3. Cypress: 중요한 사용자 흐름의 E2E 테스트

실용적인 테스트 작성 팁

  1. FIRST 원칙 따르기:
    • Fast (빠르게): 테스트는 빨리 실행되어야 함
    • Independent (독립적): 테스트끼리 의존성이 없어야 함
    • Repeatable (반복 가능): 항상 같은 결과를 내야 함
    • Self-validating (자체 검증): 테스트가 스스로 성공/실패를 판단해야 함
    • Timely (적시에): 테스트는 실제 코드 작성과 함께 이루어져야 함
  2. 테스트 가독성 높이기:
    • 명확한 테스트 이름 짓기 (given-when-then 형식 권장)
    • 하나의 테스트는 하나의 개념만 검증하기
    • 테스트 설정(setup)과 검증(assertion)을 명확히 구분하기
  3. 실용적인 테스트 커버리지 목표:
    • 100% 커버리지는 현실적이지 않을 수 있음
    • 중요한 코드와 복잡한 로직에 우선순위 두기
    • 70-80% 정도의 커버리지를 목표로 하는 것이 일반적

실제 프로젝트에 테스트 도입하기

1. 점진적으로 시작하기

  • 모든 코드를 한 번에 테스트하려 하지 마세요
  • 새 기능부터 테스트 작성을 시작하고, 중요한 기존 코드에 점진적으로 테스트 추가하기

2. CI/CD 파이프라인에 통합하기

# GitHub Actions 예시
name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Run unit and integration tests
        run: npm test
      - name: Run E2E tests
        run: npm run cypress:run

3. 테스트 문화 만들기

  • 코드 리뷰에서 테스트 코드도 함께 리뷰하기
  • 테스트 코드가 없는 PR은 머지하지 않는 규칙 만들기
  • 테스트 작성 방법에 대한 팀 내 지식 공유하기

요약

  • Jest, RTL, Cypress는 각각 다른 수준의 테스트에 적합한 도구입니다
  • 테스트 피라미드에 따라 단위, 통합, E2E 테스트의 비율을 조정하세요
  • 점진적으로 테스트를 도입하고, CI/CD 파이프라인에 통합하세요
  • 테스트는 코드의 품질과 신뢰성을 높이는 투자입니다

'Frameworks > React' 카테고리의 다른 글

React Query/TanStack 심화  (0) 2025.04.02
React 성능 분석 실전  (0) 2025.04.02
React + TypeScript 도입과 패턴  (0) 2025.04.02
React 애플리케이션 성능 최적화  (0) 2025.04.01
React 폼 관리와 유효성 검사  (0) 2025.04.01