[Next.js] Jest & React Testing Library 테스트코드

Yeong·2025년 10월 24일

Next.js 테스트코드

Jest 및 React Testing Library

React Testing Library

초기 세팅

  1. jest 설치

    > npx create-next-app@latest --example with-jest with-jest-app

  2. React Hooks Testing Library 설치

    훅을 테스트하기 위하여 설치 필요

  1. jest.setup.ts 파일을 생성하고, 아래와 같이 설정

    > import '@testing-library/jest-dom'

  2. test 파일 생성

    1. test파일은 테스트할함수파일명.test.ts(또는 js) 로 하거나, test 디렉터리 안에 있는 파일들은 모두 테스트 파일로 인식한다.
    2. 만약 특정 테스트 파일만 실행하고 싶은 경우에는npm test <파일명 이나 경로>를 입력하면 된다.

👀 -watch

👉 변경된 파일만 다시 테스트

  • Jest를 watch 모드로 실행 → 파일 저장 시마다 자동으로 관련 테스트만 재실행해줌.
  • 로컬 개발 중 계속 테스트 수정/확인 반복할 때 사용
npx jest --watch

🛠 테스트코드 종류

👀 -watchAll

👉 모든 테스트를 항상 다시 실행

  • -watch는 변경된 파일만 돌리지만,
    • -watchAll은 변경 여부 상관없이 전체 테스트를 재실행함.
  • 보통 “전체 회귀 테스트용”
npx jest --watchAll
  • Unit Test(단위 테스트)가장 작은 단위의 테스트이다.
    프론트엔드에서는 단일 컴포넌트 테스트라고도 부를 수 있다.
  • Integration Test(통합 테스트) 서로 다른 모듈, 클래스간 상호작용을 테스트한다.
    프론트엔드에서는 서로 다른 컴포넌트끼리 상호작용을 테스트하는 것을 생각할 수 있다.
  • E2E Test End To End Test를 뜻한다. 사용자의 시나리오를 테스트하는 것이다. 예를 들면, 사용자로부터 ID와 Password를 입력받고, 백엔드에 로그인 요청을 보내어 어떤 응답이 올 것인지를 테스트하는 것이다.

Jest 기본 문법

describe는 테스트 그룹을 묶어주는 역할을 하고, 그 안의 콜백함수 내에 테스트에 쓰일 가짜 변수, 객체들을 선언하여 일회용으로 사용 할 수 있다.toXxx부분에서 사용되는 함수를 흔히Test Mathcher라고 하는데, 위에서 사용된 toEqual() 함수는 값을 비교할때 사용한다.

describe('계산 테스트', () => {
   const a = 1, b = 2;

   test('a + b는 3이다.', () => {
      expect(a + b).toEqual(3);
   });
});

/*
describe('그룹 테스트 설명 문자열', () => {
   const a = 1, b = 2; // 테스트에 사용할 일회용 가짜 변수 선언

   test('개별 테스트 설명 문자열', () => {
      expect(검증대상).toXxx(기대결과);
   });
});
*/

✅ 렌더링 테스트

test / it

// 첫번째 파라미터: 작성한 테스트코드가 무엇을 하는지 이름을 정해준다
// 두번째 파라미터: 해당 테스트코드 로직
test('properly adds two numbers', () => {
  // expected result
  expect(sum(1, 2)).toBe(3);
});

// 혹은, it keyword를 사용한 테스트 코드 작성
it('properly adds two numbers', () => {
  expect(sum(1, 2)).toBe(3);
});

// render
test('알맞는 글자를 포함하여 렌더링한다.', () => {
  render(<DefaultButton>버튼</DefaultButton>);

  const button = screen.getByText('버튼');

  expect(button).toBeInTheDocument();
});
  • 동작은 둘다 동일.(it 은 test 로 별칭이 지정되어 it와 동일한 작업을 수행함)
  • 테스트 하나를 작성할 때 사용하는 키워드. (단위 테스트)
  • @testing-library/react의 render 함수는 해당 컴포넌트를 렌더링해준다.
    screen.getByTest('버튼')를 통해 "버튼" 문자열을 가지는 컴포넌트를 찾는다.
    해당 컴포넌트가 Document에 렌더링되어있다면 테스트는 통과한다.

Describe

const myBeverage = {
  delicious: true,
  sour: false,
};

describe('my beverage', () => {
  test('is delicious', () => {
    expect(myBeverage.delicious).toBeTruthy();
  });

  test('is not sour', () => {
    expect(myBeverage.sour).toBeFalsy();
  });
});
  • 테스트를 그룹화할 수 있다
    • 여러 it를 하나의 describe아래에 그룹화함.
  • 이는 필수 사항이 아니다. test최상위 수준에서 직접 블록을 작성할 수 있다. 
    그러나, 테스트를 그룹으로 구성하려는 경우 편리할 수 있다.
  • describe 블록의 각 테스트 전후에 실행할 beforeEach/afterEach 훅을 추가할 수 있다.
  • 공식문서 : https://jestjs.io/docs/api#describename-fn

Matcher 함수 종류

Jest는 다른 방법으로 값을 테스트 하도록 matcher 라는 것을 사용한다.

이는 ‘이거맞니?’ 라고 물어보는 메서드라고 보면 되는데, 기대한 값이 실제 반환된 값과 일치하는 지를 확인하는 작업이다.

테스트할 함수

// user.js

// 테스트할 함수
function getUser(id) {
  return {
    id,
    email: `user${id}@test.com`,
  };
}

module.exports = getUser;

toEqual()

객체가 일치한지 검증

const { getUser } = require('user'); // 테스트할 함수를 가져온다. 

test("return a user object", () => {
  // getUser(1)의 리턴 결과값이 { 객체 } 값이 같은 경우 true
  expect(getUser(1)).toEqual({
    id: 1,
    email: `user1@test.com`,
  });
});

toBe()

단순히 값 비교

expect(1 + 4).toBe(5);

toMatch()

문자열의 경우에는 단순히 toBe()를 사용해서 문자열이 정확히 일치하는지를 체크하지만,
종종 정규식 기반의 테스트가 필요할 때 toMatch() 함수를 사용하면 된다.

toBeCalled() / toHaveBeenCalled()

함수가 호출되었는지 여부

test("string", () => {
  expect(getUser(1).email).toBe("user1@test.com"); // 단순 문자열 비교
  expect(getUser(2).email).toMatch(/.*test.com$/); // 정규식 비교
});

toBeCalledTitmes()

함수가 몇 번 호출되었는지 검증

toReturnWith() / toHaveReturnedWith(value)

함수가 지정한 값을 반환하는지 테스트

test('drink returns La Croix', () => {
  const beverage = {name: 'La Croix'};
  const drink = jest.fn(beverage => beverage.name);

  drink(beverage);

  expect(drink).toHaveReturnedWith('La Croix'); 
  // drink(beverage) 함수 결과가 'La Croix' 문자열을 반환하는지?
});

only() / skip()

  • 테스트 코드를 디버깅할 때 유용한 함수.
  • 테스트 파일 내 테스트 함수들 중에서 하나만 실패했을 경우,그 함수만 단독으로 실행해보고 싶을 때가 있을 것이다.
    그럴 때는 해당 테스트 함수 뒤에.only() 라고 붙여 주면 Jest Runner는 해당 테스트 파일을 실행할 때.only() 가 붙은 함수만 실행해준다.
test.only("run only", () => {// 이 테스트 함수만 실행됨});
test("not run", () => {// 실행 안됨});

image.png

image.png

그럼 위와 같이 테스트 결과가 나온다.

* Expect 관련 문서 : https://runebook.dev/ko/docs/jest/expect


1️⃣ jest.fn()의 개념

jest.fn()은 Jest에서 제공하는 가짜 함수(mock function) 생성기이다.

이 함수는 실제 로직 없이, “누가 언제 어떤 인자로 호출했는지” 등의 정보를 기록한다.

즉, 테스트 시 실제 코드를 실행하지 않고도

  • 함수가 호출되었는지,
  • 몇 번 호출되었는지,
  • 어떤 인자로 호출되었는지 를 검증할 수 있게 해준다.

2️⃣ Mock 함수(Mock Function)란?

Mock 함수는 실제 함수를 대신해 동작하는 가짜 함수로,

테스트 환경에서 외부 의존성을 제거하고 함수의 호출 여부나 인자 전달만을 검증할 때 사용한다.

Mock 함수는 내부 로직을 수행하지 않고, 테스트에 필요한 호출 기록만 남긴다.

(1번 예시)

// Button.tsx

'use client';

import React from 'react';

type ButtonProps = {
    label: string;
    onClick?: () => void;
    disabled?: boolean;
};

export default function Button({ label, onClick, disabled }: ButtonProps) {
    return (
        <button onClick={onClick} disabled={disabled}>
            {label}
        </button>
    );
}
// __tests__ > Button.test.tsx
import userEvent from '@testing-library/user-event';

test("버튼을 테스트해보겠어요", () => {
		const handleBtn = jest.fn(); //가짜호출
		render (<Button label={"테스트 버튼"} onClick={handleBtn} />) // 테스트 컴포넌트를 실제 DOM에 렌더링함
		const button = screen.getByRole('button', {name: "테스트 버튼"});
		
		fireEvent.click(button);
		
		expect(handleBtn).toHaveBeenCalled();
    expect(handleBtn).toHaveBeenCalledTimes(1);
	}
)

test('버튼테스트(userEvent)', async () => {
  const user = userEvent.setup();
  const handleBtn = jest.fn();

  render(<Button label="테스트 버튼" onClick={handleBtn} />);

  const button = screen.getByRole('button', { name: '테스트 버튼' });
  await user.click(button);

  expect(handleBtn).toHaveBeenCalledTimes(1);
});
  • 테스트 대상인 Button 컴포넌트를 실제 DOM에 렌더링함.

(2번 예시)

test("value 바뀌나요?", ()=> {
    render(<DefaultInput placeholder={"값입력필요해요"} />);

    const input = screen.getByRole('textbox');
    fireEvent.change(input, { target: { value: 'value is changed' } });
    
    expect(input).toHaveValue('value is changed');
})
  • fireEvent.change(input, { target: { value: 'value is changed' } }) input 요소에 “change 이벤트”를 강제로 발생시킨다. 실제 브라우저에서 사용자가 타이핑하는 효과를 시뮬레이션 하는 것
  • { target: { value: 'value is changed' } }는 이벤트 객체의 형태로 전달된다. 즉, e.target.value = 'value is changed' 와 같은 상황을 흉내내는 것 !!!

⚙️ 내부적으로 일어나는 일

  1. <input>의 onChange 핸들러가 호출됨
  2. React가 state를 업데이트
  3. 컴포넌트가 재렌더됨
  4. input.value가 새로운 값으로 반영됨
  • expect(input).toHaveValue('value is changed') : 실제 input.value가 바뀌었는지 검증
  • ⚙️ fireEvent vs userEvent 전체적인 상호작용을 테스트한다
    • fireEvent는 프로그램적으로 이벤트를 발생시킨다.

      click()을 호출한다면 onClick 이라는 Dom Event를 발생시키는 것이다.

    • userEvent는 전체적인 상호작용을 테스트할 수 있다.

      `click()`을 호출한다면 클릭하기까지 발생할 수 있는 다양한 이벤트들을 거치게 된다.
      
      브라우저에서 사용자가 상호작용할 때 지정된 이벤트만 호출하리라는 법은 없다.
      
      `react-testing-library`로 테스트를 한다고 해도 실제로 눈에 보이는 것이 없고 브라우저 환경이 아니기 때문에 개발자가 지정한 이벤트만 모의해서 테스트하는 경우 실제 환경과 차이가 있을 수 있는데 이런점들을 최대한 보완해준 느낌인 것 같다.
      
      > 'userEvent' 예시 :
      사용자가 텍스트 상자에 입력할 때 요소에 focus 하는 이벤트가 발생하고 그 다음 키보드 및 입력 이벤트가 발생하고 입력할 때 요소의 선택 및 값이 조작된다.
      > 

      참고문서 : https://velog.io/@pds0309/react-testing-library-fireEvent-vs-userEvent

      https://testing-library.com/docs/user-event/intro/

describe('<Button />', () => {
    test.only('라벨이 올바르게 표시된다', () => {
        render(<Button label="테스트 버튼" />);
        expect(screen.getByRole('button', { name: '테스트 버튼' })).toBeInTheDocument();
    }); // only가 붙은 함수만 실행

    it('클릭 시 onClick이 호출된다', async () => {
        const onClick = jest.fn();
        const user = userEvent.setup();

        render(<Button label="클릭" onClick={onClick} />);
        const button = screen.getByRole('button', { name: '클릭' });

        await user.click(button);
        expect(onClick).toHaveBeenCalledTimes(1);
    });

    it('disabled 속성이 true면 클릭되지 않는다', async () => {
        const onClick = jest.fn();
        const user = userEvent.setup();

        render(<Button label="비활성" onClick={onClick} disabled />);
        const button = screen.getByRole('button', { name: '비활성' });

        await user.click(button);
        expect(onClick).not.toHaveBeenCalled();
        expect(button).toBeDisabled();
    });
});

1️⃣ render(<Button label="테스트 버튼" />)

  • 테스트 대상인 Button 컴포넌트를 실제 DOM에 렌더링함.
  • label="테스트 버튼"이 props로 전달돼서 내부적으로 <button>테스트 버튼</button> 같은 요소가 만들어짐.

2️⃣ screen.getByRole('button', { name: '테스트 버튼' })

  • 접근성 역할(role) 이 "button"인 요소를 찾음.
  • 동시에 버튼의 이름(name) 이 "테스트 버튼"인 걸 찾기 때문에, 실제로 화면에 "테스트 버튼"이라는 레이블을 가진 버튼만 선택됨.
  • screen은 render()로 그린 화면(DOM)을 대표하는 “가상 브라우저 창” 객체
    • 테스트 코드에서 render()를 쓰면, 그 컴포넌트가 가짜 브라우저 환경(DOM)에 렌더링된다. 즉, screen은 “렌더링된 DOM 전체를 들여다보는 창”이라고 보면 된다.

💡 단순히 텍스트로 찾는 게 아니라,

“사용자가 스크린리더로 읽을 때 ‘테스트 버튼’으로 인식되는 버튼”을 찾는 것 !!

3️⃣ .toBeInTheDocument()

  • 해당 요소가 렌더링된 DOM 트리에 존재하는지 확인.
  • 즉, "테스트 버튼"이라는 이름을 가진 버튼이 화면에 실제로 존재한다면 ✅ 통과.

getByRole과 getByText는 둘 다 DOM 요소를 찾는 Testing Library 쿼리 함수.

getByRole 은 태그명(tag name) 기준이 아닌 WAI-ARIA role 기준으로 찾음

메서드찾는 기준예시
getByRole요소의 HTML 역할(role) 기준<button>, <textbox>, <link> 등
getByText화면에 보이는 텍스트 내용 기준“로그인”, “회원가입” 등 텍스트 노드

💡 <input> 태그의 실제 Role 매핑

HTMLRole 이름getByRole() 값
<input type="text" />textbox'textbox' ✅
<input type="email" />textbox'textbox' ✅
<input type="search" />searchbox'searchbox' ✅
<input type="checkbox" />checkbox'checkbox' ✅
<input type="radio" />radio'radio' ✅
<input type="button" />button'button' ✅
<input type="file" />button'button' ✅
<textarea />textbox'textbox' ✅

추가 참고 문서

0개의 댓글