Frontend Test (1) - 단위 테스트

JungHanMa·2025년 4월 28일
0

React

목록 보기
3/3

테스트

애플리케이션의 품질과 안전성을 높이기 위해 사전에 결함을 찾아내고 수정하는 행위


테스트 코드의 효과

  • 좋은 설계에 대한 사고를 도와준다.
  • 테스트 코드를 기반으로 안정성있게 리팩토링을 할 수 있다.
  • 애플리케이션의 이해를 돕는 문서가 된다.

올바른 테스트 작성 규칙

  • 인터페이스를 기준으로 테스트를 작성
  • 100% 커버리지보다는 의미 있는 테스트인지 판단
  • 테스트 코드도 유지보수의 대상이다. 가독성을 높이자

단위 테스트

  • 단일 함수의 결과값 또는 단일 컴포넌트의 상태(UI)나 행위를 검증

  • 공통 컴포넌트는 단위 테스트에 적합 (다른 컴포넌트와 상호작용이 없고 내부 비즈로직을 기능 단위로 검증하기 좋다.)


setup과 teardown

setup: 테스트를 실행하기 전 수행해야 하는 작업

  • brforeEach, beforeAll

teardown : 테스트를 실행한 뒤 수행해야 하는 작업

  • afterEach, afterAll

React Testing Library의 핵심 철학

UI 컴포넌트를 사용자가 사용하는 방식으로 테스트 (DOM을 조회하고 사용자와 비슷한 방식으로 이벤트를 발생시킴)

  • Spy 함수는 함수의 호출여부, 인자, 반환 값 함수 호출에 관련된 다양한 값을 저장
  • 콜백함수나 이벤트 핸들러가 올바르게 호출 되었는지 검증 가능

단위 테스트 대상 선정하기

  • 별도의 로직,상태변경 없이 UI만 출력되는 컴포넌트의 경우에는 storybook
  • 로그인 상태에 따라 네비게이션 상태가 변경되는 컴포넌트는 통합테스트 할때 한번에
  • 공통 유틸함수는 단위테스트 검증한다.

모킹

  • 실제 모듈, 객체와 동일한 동작을 하도록 만든 모의 객체로 실제를 대체하는 것
  • vi.mock()을 사용해 특정 모듈을 모킹
  • 외부 모듈의 검증은 완전히 배제하고, 대상이 되는 컴포넌트의 테스트만 독립적 작성

import { screen } from '@testing-library/react';
import React from 'react';

import EmptyNotice from '@/pages/cart/components/EmptyNotice';
import render from '@/utils/test/render';

const navigateFn = vi.fn();

vi.mock('react-router-dom', async () => {
  const original = await vi.importActual('react-router-dom');
  // 실제 모듈을 모킹하여 useNavigate 훅을 spy 함수로 대체하여 반환
  return { ...original, useNavigate: () => navigateFn };
});

it('"홈으로 가기" 링크를 클릭할경우 "/"경로로 navigate함수가 호출된다', async () => {
  const { user } = await render(<EmptyNotice />);

  await user.click(screen.getByText('홈으로 가기'));
  expect(navigateFn).toHaveBeenNthCalledWith(1, '/');
});

리액트 훅 테스트 (act 함수)

리액트 훅

  • 리액트 렌더링 매커니즘을 따른 함수이기 때문에 독립적인 단위 테스트에 적합
  • testing-library/react 라이브러리 renderHook API로 검증

act

  • 상호작용 (렌더링,이펙트 등..)을 함게 그룹화하고 실행하여 실제 앱에서 동작하는 것처럼 렌더링과 업데이트를 상태 반영하도록 도와줌.
  • 업데이트 내용을 jsdom에 반영할 때 사용함.

rerender

  • prop 업데이트를 올바르게 수행하는지를 검증할 때 사용

import { renderHook, act } from '@testing-library/react';

import useConfirmModal from './useConfirmModal';

it('호출 시 initialValue 인자를 지정하지 않는 경우 isModalOpened 상태가 false로 설정된다.', () => {
  // result: 훅을 호출하여 얻은 결과 값을 반환 -> result.current 값의 참조를 통해 최신 상태를 추적할 수 있다.
  // rerender: 훅을 원하는 인자와 함께 새로 호출하여 상태를 갱신한다. (주로 props의 값이 변경되어 전달하는지 확인하는 용도)
  const { result } = renderHook(useConfirmModal);
  expect(result.current.isModalOpened).toBe(false);
});

it('호출 시 initialValue 인자를 boolean 값으로 지정하는 경우 해당 값으로 isModalOpened 상태가 설정된다.', () => {
  const { result } = renderHook(() => useConfirmModal(true));
  expect(result.current.isModalOpened).toBe(true);
});

it('훅의 toggleIsModalOpened()를 호출하면 isModalOpened 상태가 toggle된다.', () => {
  const { result, rerender } = renderHook(useConfirmModal);

  // act : 상호작용 (렌더링,이펙트 등..)을 함게 그룹화하고 실행하여 실제 앱에서 동작하는 것처럼 렌더링과 업데이트를 상태 반영하도록 도와줌.
  // 업데이트 내용을 jsdom에 반영할 때 사용함.
  act(() => {
    result.current.toggleIsModalOpened();
  });

  expect(result.current.isModalOpened).toBe(true);
});

타이머 테스트

  • 테스트 코드는 동기적으로 실행되기 떄문에 비동기적인 타이머 테스트는 모킹이 필요하다.
  • vi.useFakeTimers()을 통해 타이머 모킹, vi.advanceTimersByTime()로 시간이 흐른 것 처럼 제어 할 수 있다.
  • vi.setSystemTime(new Date('2023-12-25')) 로 현재시간 정의
  • 타이머 실행 된 후 다른 테스트 영향 주지 않기위해 vi.useRealTimers()으로 모킹을 해제한다.

userEvent vs fireEvent

fireEvent

  • 특정 요소에서 원하는 이벤트만 쉽게 발생시킬 수 있음

userEvent

  • fireEvent는 DOM 이벤트만 발생시키는 반면, useEvent는 다양한 상호 작용을 시뮬레이션 할 수있음
    • 클릭 이벤트 후 pointerdown, mousedown, focus 등등..
    • 실제 상황 처럼 disabled된 버튼이나 인풋 입력 불가능

테스트 코드 작성 시에는 userEvent를 활용해 실제 상황과 유사하도록 테스트의 신뢰성을 높히고,
지원하지 않는 부분이 있을 때, fireEvent 활용 (ex: scroll )


단위 테스트의 한계

공용 컴포넌트, 커스텀 훅, 공통 유틸처럼 다른 모듈에 대한 의존성이 거의 없을 때,
해당 모듈 자체만으로 독립적인 역할을 할 때 테스트를 진행하기 때문에 여러 모듈이 조합 되었을때
발생하는 이슈를 찾을수 없다. 또한 비즈니스 요구사항에 맞게 동작하는지 보장 할 수 없기때문에
통합 테스트, E2E 테스트등 다양한 테스트로 보강하여야 한다.

profile
Frontend Junior

0개의 댓글