jeongmingi123 님의 상세페이지[2팀 정민기] Chapter 3-1. 프론트엔드 테스트 코드

난이도에 맞는 템플릿을 선택해서 작성해주세요! Medium


Medium

7주차 과제 체크포인트

기본과제

Medium

  • 총 11개의 파일, 115개의 단위 테스트를 무사히 작성하고 통과시킨다.

질문

Q. medium.useEventOperations.spec.tsx > 아래 toastFn과 mock과 이 fn은 무엇을 해줄까요?

  • vi.fn(): 가짜 함수를 만드는 도구임.
  • enqueueSnackbarFn: vi.fn()으로 만들어진 가짜 함수 인스턴스임.
  • vi.mock(): 특정 모듈 전체를 가짜 모듈로 대체하여, 가짜 함수(enqueueSnackbarFn)가 실제 코드에 주입되도록 하는 메커니즘임

Q. medium.integration.spec.tsx > 여기서 ChakraProvider로 묶어주는 동작은 의미있을까요? 있다면 어떤 의미일까요?

  • 테마 제공: 개발자가 정의한 사용자 지정 테마를 모든 Chakra UI 컴포넌트에 적용함. 이를 통해 색상, 폰트, 간격 등 디자인 시스템을 일관되게 관리할 수 있음.
  • 전역 CSS: CSS 재설정(Reset)과 전역 스타일을 적용하여, 브라우저 간의 기본 스타일 차이를 최소화하고 모든 컴포넌트가 동일한 시작점에서 렌더링되도록 도움.
  • 접근성 지원: Chakra UI 컴포넌트들의 접근성 기능을 활성화하고 관리하는 데 필요한 컨텍스트를 제공함.

Q. handlersUtils > 아래 여러가지 use 함수는 어떤 역할을 할까요? 어떻게 사용될 수 있을까요?

테스트 로직에서 handlersUtils를 커스텀 훅으로 분리하면, MSW 핸들러를 더욱 유연하게 관리하고 테스트를 간소화함. 특히, 동적인 데이터나 테스트 케이스별로 핸들러를 오버라이드해야 할 때 유용함.

MSW 핸들러를 훅으로 관리하는 방법 handlersUtils와 같은 유틸리티 파일을 훅으로 전환하면, React 컴포넌트나 다른 훅 내에서 API 모킹을 제어할 수 있음. 핸들러 훅 생성: 먼저, useMockHandlers.js와 같은 훅을 만들어 server.use() 메서드를 호출하도록 설정합니다. 이 훅은 필요한 핸들러를 인자로 받아 server 객체에 적용함.

예제


import { useEffect } from 'react';
import { server } from '../msw/server'; // MSW 서버 인스턴스

export const useMockHandlers = (handlers) => {
  useEffect(() => {
    // 핸들러를 동적으로 적용합니다.
    server.use(...handlers);

    return () => {
      // 컴포넌트 언마운트 시 핸들러를 리셋합니다.
      server.resetHandlers();
    };
  }, [handlers]);
};

Q. setupTests.ts > 왜 이 시간을 설정해주는 걸까요?

setupTests.ts 파일에서 테스트 시간을 설정하는 것은 시간에 의존적인 테스트를 안정적으로 실행하기 위함 setupTests 내용 정리 : https://github.com/jeongmingi123/front_6th_chapter3-1/issues/1

vi.setSystemTime

vi.setSystemTime은 Vitest의 가짜 타이머(Fake Timers) 기능의 일부입니다. 이 코드는 테스트가 시작할 때 시스템의 현재 시간을 2025년 10월 1일로 고정함.

테스트 일관성을 해결 해줌. 실제 현재 시간을 사용하면 테스트를 실행할 때마다 결과가 달라질 수 있습니다. 예를 들어, '오늘'의 일정을 찾는 컴포넌트는 테스트를 실행하는 날짜에 따라 다른 결과를 반환할 것입니다. 시간을 고정하면 어떤 환경에서 언제 테스트를 실행하든 항상 동일한 결과를 얻을 수 있음.

시간 관련 로직 테스트이 가능함. '오늘로부터 7일 후', '오전 9시 이후'와 같이 시간에 의존하는 복잡한 로직을 테스트할 때, 시간을 원하는 지점으로 설정하고 vi.advanceTimersByTime() 같은 함수로 시간을 직접 조작할 수 있음.

예측 가능한 동작이 가능함. useEffect 훅 내부에서 setTimeout이나 setInterval을 사용하는 경우, 실제 시간이 흐를 때까지 기다릴 필요 없이 가짜 시간을 빠르게 진행시켜 즉시 테스트할 수 있음.

expect.hasAssertions()

expect.hasAssertions()는 테스트 내부에 최소한 하나의 expect() 호출이 있는지 확인하는 기능입니다.

이것은 버그를 방지하기 위한 좋은 습관. 예를 들어, 비동기 테스트에서 try...catch 블록을 사용했는데, try 블록이 실패하여 catch 블록으로 넘어갔지만 catch 블록에 검증 코드가 없는 경우 테스트는 에러 없이 통과할 수 있습니다.

expect.hasAssertions()를 사용하면, 이러한 경우 Jest/Vitest가 테스트에 실패를 표시하여 개발자가 놓칠 수 있는 문제를 알려줌.

심화 과제

  • App 컴포넌트 적절한 단위의 컴포넌트, 훅, 유틸 함수로 분리했는가?
  • 해당 모듈들에 대한 적절한 테스트를 5개 이상 작성했는가?

과제 셀프회고

기술적 성장

Vitest의 useFakeTimers와 시간 제어 vi.useFakeTimers()는 테스트 환경에서 시간을 멈추고 제어할 수 있게 해줌. 이를 통해 setTimeout, setInterval 같은 시간 관련 함수를 사용하는 비동기 로직을 빠르고 안정적으로 테스트할 수 있음.

주요 API 상세 설명

  1. vi.useFakeTimers() 이 함수는 테스트 환경의 모든 타이머 함수를 Vitest의 가짜 타이머로 대체합니다. 이 코드가 실행되면 실제 시간이 흐르지 않게 되며, 개발자가 시간을 직접 조작해야만 타이머 관련 콜백 함수가 실행됩니다.

  2. vi.advanceTimersByTime(ms) 이 함수는 **"일정 시간만큼 기다림"**을 시뮬레이션해요. 예를 들어, setTimeout(callback, 5000)이라는 코드가 있다면, vi.advanceTimersByTime(5000)을 호출해 실제 5초를 기다리지 않고도 callback 함수가 즉시 실행되도록 만들 수 있어요. 이는 긴 지연 시간을 가진 로직을 테스트할 때 매우 유용함

  3. vi.runAllTimers() 이 함수는 현재 예약된 모든 타이머를 즉시 실행시켜요. vi.advanceTimersByTime이 지정된 시간만큼 진행하는 반면, vi.runAllTimers는 타이머의 남은 시간을 무시하고 모든 콜백을 한 번에 실행합니다. 이는 여러 타이머가 복잡하게 얽혀 있는 상황에서 유용함

  4. vi.setSystemTime(date) 테스트 환경의 Date 객체가 반환하는 '현재 시간'을 특정 시점으로 고정합니다. 이 기능은 '오늘' 또는 '현재 시간'에 따라 다르게 동작하는 로직을 테스트할 때 필수적입니다. 예를 들어, 특정 날짜에만 보여주는 이벤트 배너를 테스트할 때, vi.setSystemTime을 사용해 그 날짜로 시간을 설정하고 테스트를 진행할 수 있음.

  5. vi.useRealTimers() 이 함수는 가짜 타이머 기능을 비활성화하고 테스트 환경을 다시 실제 시간 흐름으로 되돌립니다. 모든 테스트 케이스가 가짜 타이머를 필요로 하지는 않으므로, 필요한 경우에만 beforeAll 또는 beforeEach에서 useFakeTimers를 사용하고, afterEach에서 useRealTimers를 호출하여 다른 테스트에 영향을 주지 않도록 할 수 있음.

코드 품질

학습 효과 분석

userEvent와 같은 부분을 배울 수 있어서 좋았음. 사용자의 흐름을 어떻게 테스트를 하는지 궁금했는데, 이에 대해 해결할 수 있어서 좋았음.

userEvent 주요 메서드

  1. userEvent.click(element): 요소를 클릭함.
  2. userEvent.type(element, text): 입력란에 텍스트를 타이핑함
  3. userEvent.selectOptions(select, options): select 요소에서 하나 이상의 option을 선택
  4. userEvent.keyboard(text): 가상의 키보드 입력을 발생시킴.

userEvent

React 컴포넌트 테스트에서 사용자의 실제 상호작용을 시뮬레이션하는 데 사용되는 라이브러리임. 이는 단순히 DOM 요소를 조작하는 것(예: element.click())을 넘어, 실제 사용자가 키보드나 마우스를 사용하는 것과 같은 방식으로 브라우저 이벤트를 발생시킴

사용자의 동작 흐름을 모방해요. userEvent.click(button)은 단순히 클릭 이벤트만 발생시키는 것이 아니라, mousedown, mouseup, click과 같은 관련 이벤트들을 순차적으로 발생시킴.

예를 들어, userEvent.type(input, 'hello')는 다음과 같은 일련의 이벤트를 자동으로 발생시킴.

사용자가 입력란에 포커스(focus)

'h' 키를 누름(keydown)

'h'를 입력(input)

'h' 키를 뗌(keyup)

이러한 과정을 'e', 'l', 'l', 'o'에 대해 반복이 가능함. 이러한 세밀한 시뮬레이션 덕분에, onChange 핸들러나 onKeyDown 이벤트 같은 사용자의 동작에 의존하는 로직을 더 정확하게 테스트할 수 있음.

과제 피드백

screen를 처음 봤고, 어떻게 화면에 나타나는지 체크를 할지 몰랐음. 하지만 이번 테스트를 하면서 screen 관련된 함수에 대해 알 수 있어서 좋았음. 그 중 screen 함수에서 getByText와 findByText의 대한 부분이 헷갈렸는데, 이 부분을 정리할 수 있어서 좋았음.

screen 정의

screen을 사용하면 테스트 코드에서 실제 사용자가 보는 것과 같은 방식으로 DOM 요소를 찾을 수 있어서, 더 직관적이고 견고한 테스트를 작성할 수 있습니다. screen은 render 함수가 반환하는 객체의 모든 쿼리 함수(예: getByText, getByRole)를 포함하고 있어, 테스트 코드의 중복을 줄이고 가독성을 높여줌.

screen의 주요 쿼리 메서드

getBy... 계열 (동기적 쿼리)

  • 요소가 즉시 DOM에 존재할 때 사용해요. 찾지 못하면 에러를 발생시킴.
  1. getByRole(role, options): 접근성 역할(ARIA role)로 요소를 찾음
  2. getByText(text, options): 특정 텍스트 콘텐츠를 가진 요소를 찾음
  3. getByLabelText(text, options):
  4. getByDisplayValue(value): 폼 요소의 현재 값으로 요소를 찾음

findBy... 계열 (비동기적 쿼리)

요소가 비동기적으로 나타날 때까지 기다려야 할 때 사용함. Promise를 반환함.

  1. findByText(text, options): 특정 텍스트가 화면에 나타날 때까지 기다림 2.findByRole(role, options): 특정 역할을 가진 요소가 나타날 때까지 기다림

queryBy... 계열 (존재 유무 쿼리)

요소가 존재하지 않을 수도 있을 때 사용해요. 요소를 찾지 못해도 에러를 발생시키지 않고 null을 반환함

  1. queryByText(text): 특정 텍스트를 가진 요소가 화면에 없는지 확인할 때 사용함.

리뷰 받고 싶은 내용

안녕하세요 ! 개인적으로 궁금했던 부분이 있습니다. 기본과제는 미리 어느정도 테스트 코드해야 할 부분에 대해 알려주셔서 작성하기가 나름 괜찮았다고 생각합니다. 하지만 심화과제를 진행하려고하니까 테스트코드의 설계를 어떤식으로 해야할지 좀 고민입니다.

멘토님은 테스트 코드를 먼저 작성해두시고 그 다음에 리팩토링을 진행하시나요? 아니면 리팩토링을 하면서 테스트코드를 같이 작성하시나요.? 테스트 코드를 먼저 작성안하고 그냥 함수 구현을 먼저하고 테스트 코드를 작성하니까 뭔가 리팩토링이 잘안되는거같아서 질문드립니다.


과제 피드백

수고하셨습니다 상수님!

다음엔 회고문서도 꼭 작성하시면 좋을 것 같아요. 역랑을 궁극적으로 발전시키는데에는 동작하는 코드도 중요하지만 코드에대한 생각 정리와 과정에 대한 복기인 것 같습니다!