이번엔 이 공부 끝내겠다 시리즈/테스트코드

[Unit Test] 단위 테스트의 여러 검증법(React Testing Library, Vitest)

by GicoMomg (Lux) 2024. 5. 5.

1. 단위 테스트의 여러 검증법

  • 이번 시간에는 React, Vitest, React Testing Library를 사용해, 단위 테스트의 여러 검증방법을 알아보았다. 포스팅의 목차는 아래와 같다.

    1) 모킹으로 외부 모듈 검증하기
    2) 커스텀 훅 검증하기(ex. custom Hook)
    3) 비동기 검증하기(ex. timer)

  • 만약 단위테스트의 정의, 작성법에 대한 사전 지식이 없다면, 이 포스팅을 먼저 보는 걸 추천한다.



1) 모킹으로 외부 모듈 검증하기

(1) 모킹이 필요한 경우는?

  • 프로젝트는 여러 외부 라이브러리를 사용한다.
  • 그렇다면 테스트하고 싶은 대상 컴포넌트에서 외부 라이브러리를 사용하고 있다면 어떻게 검증해야할까?
  • 외부 라이브러리에 대한 검증도 해야할까? 검증이 필요할 수도 있지만, 대체로 생략 가능하다!

❓ 그럼, 어떤 경우에 외부 라이브러리에 대한 검증을 생략해도 될까?

  • react-router-dom 의 경우, 라이브러리 자체에서 테스트를 하고 있다. (테스트 코드)


  • 이 경우 외부 모듈에 대한 검증은 생략하고, 모듈의 특정 기능이 호출되는지만 검증하면 된다.
  • 이때 호출 검증은 모킹을 사용하면 된다.

(2) 모킹으로 외부 모듈의 호출을 검증하는 방법

📌 모킹(mocking)은 테스트 기법의 하나로, 실제 객체를 모의 객체로 대체해 테스트하는 방법이다.
모킹을 사용하면, 테스트 환경에서 외부 모듈을 테스트할 수 있다.

  • vitest에서 제공하는 vi.mock()을 사용해 특정 모듈을 모킹할 수 있다.
  • 만약 우리가 home 글자 클릭시, “/” 경로로 이동하는지 검증하고 싶다고 가정하자.
  • 그리고 경로 이동이 react-router-domuseNavigate()로 구현되어 있다면?
  • 우선, react-router-domuseNavigate()호출을 검증하기 위해, spy함수로 모킹해야한다.
import { vi } from 'vitest';

// useNavigate 훅을 spy함수로 대체
const navigateFn = vi.fn();

vi.mock('react-router-dom', async () => {
// importActual로 모듈을 모킹
  const original = await vi.importActual('react-router-dom');
  return {
    ...original,
    useNavigate: () => navigateFn,
  };
});

  • 그 다음, 테스트 코드에서는 모킹한 navigateFn()가 호출되는지 검증하면 된다!
  • 단, 외부 모듈과 동일한 모킹 객체를 생성하는 건 비용이 발생하며, 모의 객체를 남용시 테스트 신뢰성을 낮출 수 있다는 점을 유의하자.
import { render } from '@testing-library/react';

it('"home" 링크를 클릭할 시, "/"경로로 이동하는 navigate()가 호출된다.', async () => {
  // Arrange
  const { user } = await render(<NoticeHeader />);

  // Act
  await user.click(screen.getByText('home'));

  // Assert - 모킹한 함수가 호출되는지 검증만 하면 됨
  expect(navigateFn).toHaveBeenNthCalledWith(1, '/');
});

(3) 모킹시 주의할 점

  • 그런데, 만약 다른 테스트에서 외부 모듈에 대한 모킹이 필요없다면? 혹은 특정 테스트의 모킹 작업이 다른 테스트에 영향을 준다면?
  • 이 경우, 테스트의 신뢰성을 떨어뜨릴 수 있기에 모킹 초기화가 필요하다!
  • 테스트 전역 설정 파일(setupTest.js)에서 일괄 초기화하자.
clearAllMocks resetAllMocks
- 모킹된 모의 객체 호출에 대한 히스토리를 초기화
- 단, 모듈의 모킹은 유지됨
-모킹 모듈 기반으로 작성된 테스트 코드를 작성할 때 유용
- 반면, 모킹 히스토리가 쌓이므로 다른 테스트에 영향을 줄 수 있음
- 모킹 모듈에 대한 모든 구현을 초기화
- 테스트의 안전성과 신뢰성을 보장
// setupTest.js

afterEach(() => {
  vi.clearAllMocks();
});

afterAll(() => {
  vi.resetAllMocks();
});



2) 커스텀 훅 검증하기(ex. custom Hook)

(1) 커스텀 훅이란?

  • 커스텀 훅이란, 특정 로직을 재사용 가능한 형태로 캡슐화하는 방법이다.
  • 커스텀 훅을 사용하면, 로직의 재사용성이 높아지며 비즈니즈 로직을 분리되기에 결합도를 낮출 수 있다.
  • 커스텀 훅의 검증은 React Testing Library의 renderHook()로 가능하다.

(2) 커스텀 훅을 검증하는 방법

  • 커스텀 훅 검증법을 알아보기 위해, 알림 모달의 상태를 관리하는 useAlertModal을 가져왔다.
  • 이제, 2가지 테스트 케이스를 보면서, 커스텀 훅 검증 코드를 살펴보자.
// useAlertModal.tsx

import { useState } from 'react';

const useAlertModal = (initialValue = false) => {
  const [isModalOpened, setIsModalOpened] = useState(initialValue);

  const toggleIsModalOpened = () => {
    setIsModalOpened(!isModalOpened);
  };

  return {
    toggleIsModalOpened,
    isModalOpened,
  };
};

export default useAlertModal;

[테스트 케이스 1] initialValue가 지정되지 않는 경우 isModalOpened의 상태가 false인지

  • renderHook()은 커스텀 훅을 렌더링하고, 반환된 훅을 테스트할 수 있게 해준다.
const { result, rerender } = renderHook(커스텀 훅);
반환값 설명
result result 객체는 훅의 현재 반환 값에 접근할 수 있게 해줌 (ex. result.current )
rerender rerender()는 훅을 다시 렌더링할 수 있는 함수임
테스트 중 훅에 새로운 props를 제공하거나, 상태를 업데이트하여 그 변화를 확인 할 때 유용함

  • renderHook()useAlertModal을 넘겨, 훅의 상태값을 검증할 수 있다.
import { renderHook } from '@testing-library/react';
import useAlertModal from './useAlertModal';

it('initialValue 인자가 지정되지 않는 경우 isModalOpened의 상태가 false가 된다.', () => {
  // Arrange
  const { result, rerender } = renderHook(useAlertModal);

  // Assert
  expect(result.current.isModalOpened).toBe(false);
});

[테스트 케이스 2] toggleIsModalOpened()를 호출하면, isModalOpened 상태가 변경되는지

  • act()는 컴포넌트가 렌더링된 후, 상태를 변경하는 함수를 호출할 때 사용한다.
  • 만약 render() 혹은 useEvent 모듈을 사용하지 않는 경우, act()에서 상태 변경 함수를 호출해야한다.
act(() => {
  // 상태 변경 함수 호출
})

  • act() 에서 toggleIsModalOpened()를 호출해, 커스텀 훅의 상태를 변경하고 이를 검증할 수 있다.
import { renderHook, act } from '@testing-library/react';
import useAlertModal from './useAlertModal';

it('toggleIsModalOpened()를 호출하면, isModalOpened 상태가 toggle된다.', () => {
  // Arrange
  const { result } = renderHook(() => useAlertModal(true));

  // Act
  act(() => { 
    result.current.toggleIsModalOpened();  // (c)
  });

  // Assert
  expect(result.current.isModalOpened).toBe(false);
});



3) 비동기 검증하기(ex. timer)

  • 일반적으로 테스트 코드는 동기적으로 실행된다.
  • 그래서 비동기 함수가 실행되기도 전에 테스트가 종료되어 테스트가 실패할 수 있다.
  • 그럼 어떻게 비동기 함수를 검증할 수 있을까? 바로 타이머를 모킹하면 된다!

(1) 비동기를 검증하는 방법, debounce 검증해보기

  • debounce()는 일정 시간이 지난 후에 한 번만 실행되는 함수이다.
  • 주로 스크롤 이벤트나 입력창 입력 이벤트에 적용하는데, 로직의 호출 횟수를 절약해준다.
  • 그럼 debounce()의 동작을 검증하기 위해선 어떻게 해야할까? 순서대로 알아보자.
export const debounce = (fn, wait) => {
  let timeout = null;

  return (...args) => {
    const later = () => {
      timeout = -1;
      fn(...args);
    };
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = window.setTimeout(later, wait);
  };
};

첫 번째, 타이머 모킹 준비하기

  • debounce()는 타이머 함수를 사용한다. 그래서 함수를 검증하기 위해, 타이머를 모킹해야한다.
  • useFakeTimers()은 setTimeout, setInterval, clearTimeout, clearInterval 함수를 모킹할 수 있다.
  • beforeEach에서 useFakeTimers() 를 호출해 타이머를 모킹해주자.

describe('debounce', () => {
  beforeEach(() => {
    // 타이머를 조작할 준비
    vi.useFakeTimers();
  });

  afterEach(() => {
    // 테스트 후 타이머를 원상복귀시킴
    vi.useRealTimers();
  });
  ...
});

두 번째, 타이머를 조작해, 테스트 검증하기

  • 앞서, useFakeTimers() 로 타이머 조작 준비를 했다. 이제 실제로 타이머를 조작해보자!
  • advanceTimersByTime(time)을 사용하면 time초가 지난 것처럼 시뮬레이션 할 수 있다.
vi.advanceTimersByTime(time);

  • 만약 300ms 이후에 debounce()가 실행되는지 테스트하고 싶다면?
  • 아래 코드와 같이 advanceTimersByTime의 인자값을 300으로 설정하면 검증할 수 있다.

describe('debounce', () => {
  ...
  it('특정 시간이 지난 후 함수가 실행된다', () => {
    // Arrange
    const spy = vi.fn();
    const time = 300;
    const debounced = debounce(spy, time);

    // Act
    debounced();
    vi.advanceTimersByTime(time);

        // Assert
    expect(spy).toHaveBeenCalled();
  });
  ...
});



2. 마치며…

  • 이번 시간에는 단위테스트의 여러 검증법으로, 모듈 검증, 커스텀 훅 검증, 비동기 검증법을 알아보았다.
  • 먼저, 모듈 검증의 경우 실제로 모듈 자체를 검증하기 보다는 모듈의 호출을 검증했다. 그 이유는 외부 모듈의 경우 자체적으로 검증을 거치기 때문이었는데 이 경우 모듈 내부 로직이 아닌 호출만 검증하면 된다.
  • 두 번째는 커스텀 훅을 검증하는 법을 살펴봤다. 커스텀 훅은 로직을 캡슐화해 재사용성을 높이는 방법으로, 로직 결합도를 낮출 수 있는 장점이 있다. 커스텀 훅의 경우 테스트를 위해 renderHook, act를 사용해야 했다.
  • 마지막으로 비동기 검증의 경우, 타이머 예시를 살펴보았다. 테스트는 동기적으로 작동하기에 일정 시간 후에 함수를 테스트하기 어렵다. 그래서, 비동기 함수를 테스트하기 위해 useFakeTimers로 타이머를 조작했다.
  • 이처럼 단위 테스트에는 여러 검증법이 있는데, 더 많은 방법이 있기에 공식문서나 관련 자료를 더 찾아보는 걸 추천한다 🙂



반응형

댓글