본문 바로가기

리액트 심화 스터디

[Week 8] Jest, React Testing Library

TDD(Test Driven Development: 테스트 주도 개발)

TDD는 개발자가 새로운 기능이나 함수에 대한 테스트 케이스를 먼저 작성하고, 그 테스트를 통과하기 위한 최소한의 코드를 작성하는 개발 기법이다. 이 방법론은 코드의 기능에 초점을 맞추며, 개발자의 관점에서 테스트 케이스를 작성한다.

  • 실패하는 테스트 작성: 구현할 기능에 대한 테스트 케이스를 작성하여 초기 상태에서 테스트가 실패하도록 만든다.
  • 테스트를 통과하도록 수정: 작성한 테스트를 통과시키기 위해 필요한 최소한의 코드를 구현한다.
  • 리팩토링: 통과한 테스트를 기반으로 코드의 품질을 개선하고 가독성을 높인다.

BDD(Behavior Driven Development: 행동 주도 개발)

BDD는 TDD에서 파생된 개발 방법론으로, 테스트 케이스 자체가 요구 사항이 되도록 하는 개발 방법을 말한다.

이는 사용자의 관점에서 시스템의 행동에 초점을 맞추어 테스트 케이스를 작성한다.

BDD의 구조

  • Given: 주어진 환경이나 조건을 설정한다.
    • 이 단계에서는 테스트를 수행하기 전에 필요한 초기 상태를 정의한다.
  • When: 특정 행위를 수행한다.
    • 사용자가 어떤 행동을 취하는지를 설명하는 단계이다.
  • Then: 기대하는 결과를 검증한다.
    • 이 단계에서는 사용자가 수행한 행위에 대한 결과를 확인한다.

 

1. Jest

📌 Jest란?

Jest는 페이스북(Facebook)에서 개발한 JavaScript 테스트 프레임워크이다.

특히 React와 호환성이 뛰어나며, Babel, TypeScript, Node.js, Angular, Vue 등 다양한 프로젝트에서 활용이 가능하다.

이를 통해 단위 테스트(Unit Test), 통합 테스트(Integration Test), 스냅샷 테스트(Snapshot Test) 등을 수행할 수 있다.

 

📌 Jest를 사용하는 이유는 무엇일까

Jest가 등장하기 이전에는 여러 자바스크립트 테스팅 라이브러리를 조합해서 사용해야 했다.

Test Runner - Mocha, Jasmin
Test Matcher - Chai, Expect
Test Mock - Sinon, Testdouble

 

Jest는 이러한 모든 기능을 하나의 통합된 테스트 프레임워크로 제공한다!

 

📌 주요 특징

  • 간편한 설정과 빠른 실행 속도
    Jest는 초기 설정이 거의 필요 없으며, 기본 설정만으로도 다양한 테스트 시나리오를 지원한다.
    또한, 테스트를 병렬로 실행하여 빠른 피드백을 제공한다.
  • 스냅샷 테스팅
    UI 컴포넌트의 렌더링 결과를 스냅샷으로 저장하고, 이후 변경 사항과 비교하여 의도하지 않은 변경을 쉽게 감지할 수 있다.
  • 테스트 커버리지 리포트
    코드 커버리지 리포트를 생성하여 테스트가 코드의 어느 부분을 커버하고 있는지 파악할 수 있다.
  • 자동화된 테스트 실행과 상세한 결과 제공
    Jest는 자동으로 테스트를 실행하고, 실패한 테스트에 대해 상세한 오류 메시지와 함께 컨텍스트를 제공하여 문제를 신속하게 해결할 수 있게 해준다.

 

📌 설치 방법

1. Jest 설치: yarn add --dev jest 명령어로 Jest 설치

2. package.json 수정

// package.json
{
  "scripts": {
    "test": "jest"
  }
}

 

3. 테스트 실행: yarn test 명령어로 Jest 테스트 실행

 

📌 테스트 파일 인식 기준

Jest는 특정 패턴을 가진 파일을 자동으로 테스트 파일로 인식하여 실행한다.

  1. 파일명에 .test.js 또는 .spec.js가 포함된 경우
  2. __tests__ 폴더 내에 있는 파일
  3. tests 폴더 안에 있는 파일

예를 들어, add.test.js, add.spec.js, __tests__/add.js와 같은 파일이 테스트 파일로 인식된다.

 


📌 Jest의 테스트 코드 작성 방식

Jest를 사용하여 테스트를 작성하는 방식에는 여러 가지가 있다.

1️⃣ 개별 테스트 나열 방식

- test : 단일 테스트 케이스 작성

test('1과 2를 더하면 3이 된다', () => { // 테스트에 대한 설명
  expect(add(1, 2)).toBe(3); // 예상값과 실제값 비교
});

2️⃣ BDD 스타일 테스트 조직화

BDD 스타일은 describe와 it 함수를 사용하여 테스트를 더 구조화하고 가독성을 높이는 방식이다.

- describe : 테스트의 큰 범주 정의, 기능 단위의 테스트를 진행할 때 관련 테스트를 그룹화

describe('add 함수 테스트', () => {
  it('1과 2를 더하면 3이 된다', () => {
    expect(add(1, 2)).toBe(3);
  });

  it('음수를 더하면 올바른 값을 반환한다', () => {
    expect(add(-1, -2)).toBe(-3);
  });

  it('0을 더하면 다른 수가 그대로 반환된다', () => {
    expect(add(0, 5)).toBe(5);
  });
});

it 함수와 test 함수는 동일한 기능을 한다. 따라서, 위 코드에서 it 대신 test를 사용해도 괜찮다.

 


📌 Jest의 Matchers

Matcher는 테스트의 예상 결과를 정의하는 데 사용되는 함수이다.

Jest에서 expect 함수와 함께 사용되어 실제 값이 예상한 값과 일치하는지 검증한다.

  • toBe: 기본적인 동등성 검사 (원시 값 비교, 동일 객체 참조 확인)
expect(add(1, 2)).toBe(3);
  • toEqual: 객체나 배열의 내용 동등성 검사
const obj = { a: 1, b: 2 };
expect(obj).toEqual({ a: 1, b: 2 })
  • toBeNull, toBeUndefined, toBeTruthy, toBeFalsy: 특정 값 상태 검사
const value = null;
expect(value).toBeNull()
  • toContain: 배열이나 문자열에 특정 값 포함 여부 검사
const fruits = ['apple', 'banana', 'cherry'];
expect(fruits).toContain('banana');
  • toMatch: 정규식이나 문자열 패턴 매칭 검사
const email = 'test@example.com';
expect(email).toMatch(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/);

 


📌 Jest의 테스트 커버리지

테스트 커버리지(Test Coverage)는 소프트웨어 테스트에서 테스트가 코드의 어느 부분을 실행하고 검증하는지를 측정하는 지표 중 하나이다. 이를 통해 개발자는 테스트가 코드의 모든 중요한 부분을 제대로 검증하고 있는지, 또는 어떤 부분이 테스트되지 않아 잠재적인 버그가 존재할 가능성이 있는지를 파악할 수 있다.

"scripts" : {
	"coverage": "jest --coverage"
}

 

yarn coverage를 통해 커버리지 확인❗️

 

커버리지가 높다면 코드의 대부분 또는 모든 부분이 테스트에 의해 검증되었음을 의미하기 때문에

소프트웨어의 안정성과 신뢰성이 높다고 볼 수 있다.

 


📌 Jest의 Mock 기능

모킹(Mock)은 단위 테스트에서 특정 함수나 모듈을 실제 구현 대신 가짜(mock)로 대체하여 외부 의존성을 분리한 채 특정 동작을 시뮬레이션할 수 있는 기능이다.

함수 모킹

jest.fn()을 사용하여 함수 모킹

이 모킹된 함수는 호출 여부나 호출된 횟수, 호출된 인자 등을 추적할 수 있게 해준다.

const mockFn = jest.fn(); // mockFn을 가짜 함수로 설정

mockFn(); // 함수를 호출

expect(mockFn).toHaveBeenCalled();  // mockFn이 호출되었는지 검사

모듈 모킹

jest.mock()을 사용하여 외부 모듈 모킹

axios를 모킹하여 외부 API 호출을 테스트

axios.get.mockResolvedValue를 사용하여 axios.get 호출 시 반환될 가짜 데이터를 설정

실제로 API를 호출하지 않고, 가짜 데이터를 이용해 테스트를 진행하는 방식

jest.mock('axios');
import axios from 'axios';

test('데이터를 가져와야 한다', async () => {
  axios.get.mockResolvedValue({ data: { user: 'Alice' } });
  const data = await fetchData();
  expect(data.user).toBe('Alice');
});

 


2. React Testing Library

React Testing Library는 React 컴포넌트의 테스트를 보다 쉽고 직관적으로 작성할 수 있도록 도와주는 라이브러리이다. RTL은 행위 주도 테스트(Behavior Driven Testing) 방식을 채택하여, 사용자의 실제 상호작용을 기반으로 컴포넌트를 테스트한다. 즉, 컴포넌트의 내부 구현보다는 사용자 경험을 중심으로 테스트를 진행한다.

📌 RTL의 주요 특징

  • 사용자 중심의 테스트

실제 사용자가 애플리케이션을 사용하는 방식과 유사하게 테스트를 작성한다.

  • 내부 구현에 대한 의존성 감소

DOM 요소의 구조나 클래스 이름보다는 사용자에게 보여지는 결과에 집중한다.

  • Jest와의 호환성

RTL과 Jest는 상호 보완적인 관계이다. Jest는 기본적인 테스트 인프라와 API를 제공하고, RTL은 React 컴포넌트를 사용자 관점에서 테스트하기 위한 추가 도구를 제공하므로, 이 둘을 함께 사용하는 것이 보편적이다.

 

📌 설치 및 설정

Create React App(CRA)을 사용하는 경우 기본적으로 RTL이 내장되어 있어 별도의 설정이 필요 없다.

Create React App을 사용하지 않는 경우, RTL을 설치하려면 다음 명령어를 실행해야 한다.

yarn add --dev @testing-library/react @testing-library/jest-dom

 

설치 후, Jest 설정 파일에 @testing-library/jest-dom을 추가하여 추가적인 매처를 사용할 수 있도록 설정해야 한다.

// setupTests.js
import '@testing-library/jest-dom';

📌 기본 사용법

RTL을 사용하여 리액트 컴포넌트를 테스트하는 기본적인 방법을 살펴보자.

import React from 'react';

const Hello = ({ name }: { name: string }) => {
  return <div>Hello {name}</div>;
};

export default Hello;

 

위 컴포넌트를 테스트하는 코드는 다음과 같다.

import React from 'react';
import { render, screen } from '@testing-library/react';
import Hello from './Hello';

test('renders hello message', () => {
  render(<Hello name="World" />);
  const helloElement = screen.getByText(/Hello World/i);
  expect(helloElement).toBeInTheDocument();
});
  1. render: 테스트할 컴포넌트 렌더링
  2. screen.getByText: 화면에 특정 텍스트가 존재하는지 확인
  3. expect(...).toBeInTheDocument(): 해당 요소가 DOM에 존재함을 검증

 

💡 screen 객체

screen 객체 렌더링된 DOM에 접근할 수 있는 주요 방법을 제공한다.

아래와 같은 다양한 쿼리 메서드를 사용하여 요소를 가져올 수 있다.

  • getByText
  • getByRole
  • getByLabelText
  • getByPlaceholderText
  • getByTestId 등

 

📌 주요 함수와 활용 예제

RTL의 다양한 유틸리티 함수를 통해 사용자 상호작용을 시뮬레이션하고 컴포넌트를 테스트할 수 있다.

사용자 상호작용을 시뮬레이션할 때 활용되는 주요 함수와 테스트 코드를 알아보자!

1️⃣ fireEvent

fireEvent 간단한 사용자 이벤트를 시뮬레이션하는 함수이다.

다음은 입력 창에 값을 입력하는 테스트 코드이다.

// InputComponent.tsx
import React, { useState } from 'react';

const InputComponent = () => {
  const [value, setValue] = useState('');

  return (
    <div>
      <label htmlFor="name">Name:</label>
      <input
        id="name"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <p>{value}</p>
    </div>
  );
};

export default InputComponent;
// InputComponent.test.ts
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import InputComponent from './InputComponent';

test('updates value on input change', () => {
  render(<InputComponent />);

  const input = screen.getByLabelText(/name/i); // 레이블 텍스트 "name"과 연결된 <input> 요소
  fireEvent.change(input, { target: { value: 'New Name' } });
  // fireEvent.change는 요소의 변경 이벤트를 발생시킨다.

  expect(screen.getByText('New Name')).toBeInTheDocument();
});

 

- getByLabelText는 레이블과 폼 요소가 htmlFor 속성(또는 래핑)으로 올바르게 연결되어 있을 때만 작동한다.

2️⃣ userEvent

userEvent fireEvent보다 더 실제 사용자와 유사한 이벤트를 시뮬레이션할 수 있는 유틸리티이다.

fireEvent와는 달리 비동기 작업을 포함한 복잡한 상호작용을 처리할 수 있다.

다음은 버튼을 클릭하는 테스트 코드이다.

// ButtonComponent.tsx
import React, { useState } from 'react';

const ButtonComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>클릭!</button>
      <p>{count}번 클릭</p>
    </div>
  );
};

export default ButtonComponent;
// ButtonComponent.test.ts
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ButtonComponent from './ButtonComponent';

test('버튼 클릭 시 카운트가 증가합니다', async () => {
  render(<ButtonComponent />);

  const button = screen.getByText(/클릭!/i);
  await userEvent.click(button);
  // userEvent.click은 사용자가 특정 요소(예: 버튼)를 클릭하는 행동을 나타낸다.

  expect(screen.getByText(/1번 클릭/i)).toBeInTheDocument();
});