TDD/JEST+React Test

TDD(Test Driven Development)와 테스팅 도구 Jest, React Testing Library

lamarcK 2025. 4. 16. 16:47

TDD

TDD(Test Driven Development)는 테스트 주도 개발을 의미한다.

일종의 개발 방법론인데 정식 코드를 먼저 만들지 않고 테스트 코드를 만든다음에 테스트를 통과한 이후에 정식 코드를 개선하는 방식으로 개발을 하는 방법이다.

 

테스트를 먼저 작성하고, 그 후에 실제 코드를 개발 : Red(실패) → Green(성공) → Refactor(개선) 단계를 반복한다.

실패하는 테스트 작성 → 테스트를 통과하는 최소한의 코드 작성 → 코드 리팩토링 순서로 진행된다.

 

테스트가 성공한다는 것은 1차적으로 코드가 작동한다는 것을 의미하기 때문에 아래와 같은 장점이 있다.

  • 코드의 품질과 신뢰성 향상
  • 버그 발생 가능성 감소
  • 유지보수가 용이
  • 과도한 설계를 방지

또한 이런 테스트를 위한 도구로는 Jest나 React Testing Library같은 것이 있다.


Jest

Jest 란 JavaScript 코드의 정확성을 검증하기 위한 테스팅 프레임워크다.

특히 React 애플리케이션의 컴포넌트, 함수, 비동기 작업 등의 동작을 테스트하는데 주로 사용된다.

 

일반적으로 우리가 컴포넌트를 만들 때 한번에 원하는 기능이 동작하는 것은 어려운 일이다. 타입 오류도 발생할 수 있고, 데이터 전달 과정에서 흐름이 문제가 생길 수도 있고 특정 데이터에만 해당하는 오류가 생길 수도 있다.

예를 들면 특정 숫자에 반응하는 CSS를 만들려고 할 때도 실제로 동작하는지 하지 않는지는 우리가 직접 데이터를 넣어보거나 실험을 해봐야 알 수 있다.

물론 변수를 사용해서 데이터를 넣어보거나 할 수 있지만 랜덤한 수치에 반응하는 컴포넌트를 만들 때엔 어떻게 해야할까?

또 다양한 수치에 반응하는 컴포넌트를 만들 때는 어떻게 해야할까?

혹은 특정 좌표를 눌렀을 때의 동작이 어떻게 되는지 코드 상에서 테스트가 가능할까?

 

이것을 코드 자체에서 일일히 테스트 하는 것은 공수가 너무 많이 들어 불가능하다. 때문에 우리가 원하는 기능을 정말 구현하려면 어떤 특정한 행동이나 값을 넣어서 실제로 제대로된 동작을 하는지 확인을 해야하는데 그것을 할 수 있도록 만들어진 테스트용 도구가 JEST다.

 

예를 들어서 카운트에 따라서 손뼉을 치는 컴포넌트를 만들어야 한다고 쳐보자. 맞다. 369 게임이다. 그런데 이런 컴포넌트를 만들어 놓고 웹페이지에 띄워 놓은 상태로 일일히 버그가 나는 경우나 특정 상황을 테스트하는 것은 어렵다. 우리가 일반적으로 아는 것과 다른 규칙을 적용해본다면 어떨까?

예를 들면 3의 배수일때 박수 치기, 3이 들어가는 숫자에 박수 치기 등이다. 간단한 컴포넌트지만 검증해야 하는 부분들이 존재하기 때문에 코드 상으로는 테스트가 힘들다.

// React의 클라이언트 사이드 렌더링을 위한 지시어
'use client'

// React의 useState 훅 임포트
import { useState } from 'react';

// React 함수형 컴포넌트 선언 (TypeScript 사용)
const ClapCounter: React.FC = () => {
  // count 상태와 이를 변경할 수 있는 setCount 함수 선언
  // 초기값은 0으로 설정하고, number 타입으로 지정
  const [count, setCount] = useState<number>(0);

  // 박수 이모지를 보여줄지 결정하는 함수
  // 숫자를 받아서 boolean을 반환
  // 3의 배수이거나 숫자에 3이 포함되면 true 반환
  const shouldShowClap = (num: number): boolean => {
    return num % 3 === 0 || num.toString().includes('3');
  };

  // 컴포넌트의 UI를 렌더링
  return (
    <div>
      {/* shouldShowClap 함수가 true를 반환할 때만 박수 이모지 표시 */}
      {shouldShowClap(count) && <div data-testid="clap">👏</div>}

      {/* 현재 카운트 값을 표시 */}
      <div data-testid="count-display">Count: {count}</div>

      {/* 
        버튼 클릭 시 카운트를 1 증가
        data-testid는 테스트 코드에서 요소를 식별하기 위한 속성
      */}
      <button 
        onClick={() => setCount(count + 1)}
        data-testid="increment-button"
      >
        Click me
      </button>
    </div>
  );
};

// 컴포넌트 내보내기
export default ClapCounter;

하지만 Jest를 사용하면 이런 검증 과정을 상당히 편하게 할 수 있다.

describe('ClapCounter Component', () => {
  test('초기 카운트는 0이며, 박수 표시가 없어야 함', () => {
    render(<ClapCounter />);
    expect(screen.getByTestId('count-display')).toHaveTextContent('Count: 0');
    expect(screen.queryByTestId('clap')).not.toBeInTheDocument();
  });
  • 위의 코드는 크게 3부분으로 나누어져 있다.
  • 구조 : 그룹화 및 테스트 설명 부분
    • describe('그룹 설명', () => { // 테스트 그룹화 // });
    • test('테스트 설명', () => { // 개별 테스트 // });
    • ClapCounter 컴포넌트와 관련된 테스트들을 하나의 그룹으로 묶는다.
    • "초기 카운트는 0이며, 박수 표시가 없어야 함"이라는 동작을 검증한다.
    • 실제로 그냥 설명용이지 동작을 하는 것은 아니다.
  • 가상 dom 관련
    • render : 컴포넌트 렌더링용
    • screen : 렌더링된 DOM에 대한 쿼리 메소드를 담고 있음
    • 먼저 ClapCounter 컴포넌트를 가상 DOM에 렌더링한다.
  • 기대값 검증
    • expect(실제값).matcher(기대값)
    • expect에 실제값을 넣고 matcher 부분에 기댓값을 넣어서 원하는 결과와 같은지 확인한다.
    • 'count-display'라는 test-id를 가진 요소를 찾아서 그 텍스트가 'Count: 0'인지 확인한다.
    • 'clap'이라는 test-id를 가진 요소가 문서에 존재하지 않는지 확인한다.

여기서 getByTestId('count-display')나 queryByTestId('clap') 부분은 실제 ClapCounter 컴포넌트 코드에서 테스트에 사용하기 위해 data-testid 속성으로 지정해둔 값이다. 실제 렌더링 되어도 html 요소에 어떤 영향도 주지 않는 테스트 목적으로만 사용되는 속성이다.


그렇다면 다수의 expect는 어떻게 작용하는가?

expect(screen.getByTestId('count-display')).toHaveTextContent('Count: 0');
expect(screen.queryByTestId('clap')).not.toBeInTheDocument();

 

Jest에서 하나의 test 블록 안에 여러 개의 expect가 있을 때, 이들은 순차적으로 실행되지만 서로 독립적으로 평가된다.

  • 첫 번째 expect가 실패하더라도 두 번째 expect는 계속 실행된다.
  • 각 expect는 독립적으로 평가되어 별도의 실패/성공을 보고한다.
  • 전체 결과
    • test 블록 내의 모든 expect 중 하나라도 실패하면 해당 테스트는 실패로 처리된다.
    • 모든 expect가 성공해야 테스트가 통과된다.

실제 테스트 실행

//패키지 매니저별 실행 명령어:
npm test
yarn test
pnpm test

//직접 Jest 실행:
jest
npx jest
pnpx jest

실제로 ClapCounter.test.tsx 형식으로 파일을 만들고 테스트 명령어를 터미널에 입력하면 해당 코드에 대한 검증을 해준다.

// ClapCounter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import ClapCounter from '@/components/ClapCounter';

describe('ClapCounter Component', () => {
  // 테스트 1: 초기 상태 확인
  // 실패 원인: 0도 3의 배수로 인식되어 박수가 표시됨
  // 수정 방향: 테스트 기대값을 수정하거나, 0에서도 박수가 표시되는 것을 허용
  test('초기 카운트는 0이며, 박수 표시가 없어야 함', () => {
    render(<ClapCounter />);
    // 카운트가 0인지 확인 - 성공
    expect(screen.getByTestId('count-display')).toHaveTextContent('Count: 0');
    // 박수 표시가 없어야 함 - 현재 실패 중
    expect(screen.queryByTestId('clap')).not.toBeInTheDocument();
  });

 

실제로 15번 줄에서 실패 처리되어서 결과가 실패로 반환됐다.

expect(screen.queryByTestId('clap')).not.toBeInTheDocument();

만약에 해당 검증 부분을 주석처리하거나 삭제하고 다시 테스트를 하면 패스 처리된다.

Jest의 주요 테스트 메서드

**1. 테스트 구조 관련 메서드
describe('그룹 설명', () => {
// 테스트 그룹화
});

test('테스트 설명', () => {
// 개별 테스트
});

it('테스트 설명', () => {
// test()의 별칭
});

beforeAll(() => {
// 모든 테스트 전에 1번 실행
});

afterAll(() => {
// 모든 테스트 후에 1번 실행
});

beforeEach(() => {
// 각 테스트 전에 실행
});

afterEach(() => {
// 각 테스트 후에 실행
});
**2. 기대값 검증 메서드 (Matchers)
expect(value).toBe(other);              // 엄격한 동등 비교 (===)
expect(value).toEqual(other);           // 재귀적 동등 비교
expect(value).toBeDefined();            // undefined가 아닌지 확인
expect(value).toBeUndefined();          // undefined인지 확인
expect(value).toBeNull();               // null인지 확인
expect(value).toBeTruthy();             // true로 평가되는지 확인
expect(value).toBeFalsy();              // false로 평가되는지 확인
expect(value).toContain(item);          // 배열이나 문자열에 항목 포함 여부
expect(value).toBeGreaterThan(number);  // 큰 수 비교
expect(value).toBeLessThan(number);     // 작은 수 비교
**3. 비동기 테스트 메서드
// Promise 테스트
test('async test', () => {
return somePromise().then(data => {
 expect(data).toBe('value');
});
});

// async/await 테스트
test('async test', async () => {
const data = await somePromise();
expect(data).toBe('value');
});

// done 콜백
test('async test', done => {
someAsyncFunction(() => {
expect(result).toBe('value');
done();
});
});
**4. 모킹 관련 메서드
```javascript
// 함수 모킹
jest.fn();                              // 모의 함수 생성
jest.spyOn(object, methodName);         // 메서드 감시
jest.mock('./modulePath');              // 모듈 모킹

// 모의 함수 검증
expect(mockFn).toHaveBeenCalled();              // 호출 여부
expect(mockFn).toHaveBeenCalledTimes(number);   // 호출 횟수
expect(mockFn).toHaveBeenCalledWith(arg1, arg2);// 호출 인자
**5. 예외 처리 테스트
// 예외 발생 테스트
expect(() => {
throwingFunction();
}).toThrow();

expect(() => {
throwingFunction();
}).toThrow('specific error message');
**6. 객체 매칭 메서드
// 객체 부분 일치 검사
expect(object).toMatchObject({
  property: value
});

// 객체 스냅샷 테스트
expect(object).toMatchSnapshot();

// 객체 속성 존재 확인
expect(object).toHaveProperty('property');
expect(object).toHaveProperty('property.nested');
**7. 숫자 관련 메서드
expect(number).toBeCloseTo(value, numDigits); // 부동소수점 비교
expect(number).toBeGreaterThanOrEqual(number);
expect(number).toBeLessThanOrEqual(number);
**8. 배열/반복가능 객체 메서드:
expect(array).toContain(item);
expect(array).toHaveLength(number);
expect(array).toEqual(expect.arrayContaining([items]));