hyunzsu 님의 상세페이지[8팀 현지수] Chapter 3-1. 프론트엔드 테스트 코드

HARD

7주차 과제 체크포인트

기본과제

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

질문

Q. handlersUtils에 남긴 질문에 답변해주세요.

// 문제 상황
const eventsData = [...events]; // 모든 테스트가 공유하는 전역 변수

// 테스트 A 실행
test('이벤트 생성', () => {
  eventsData.push(newEvent); // 전역 배열 수정
});

// 테스트 B 실행 (동시)
test('이벤트 목록 조회', () => {
  expect(eventsData).toHaveLength(2); // 예상: 2, 실제: 3 (테스트 A 영향)
});

문제 원인

MSW 핸들러 내부의 전역 변수가 문제의 원인이고, 테스트 간 데이터 상태가 공유되어 테스트 실행 순서에 따라 결과가 달라지는 동작이 발생했습니다.

독립적 데이터 환경 구축

// 해결: 각 테스트마다 독립적인 데이터 복사본 생성
export const setupMockHandlerCreation = (initEvents = []) => {
  const eventsData = [...initEvents]; // 매번 새로운 배열 생성
  
  return server.use(
    http.get('/events', () => Response.json(eventsData)),
    http.post('/events', async ({ request }) => {
      const newEvent = await request.json();
      eventsData.push(newEvent); // 이 테스트만의 독립적인 배열에 추가
      return Response.json(newEvent, { status: 201 });
    })
  );
};

목적별 핸들러 분리

// 생성 테스트용: GET + POST만
setupMockHandlerCreation(initEvents)

// 수정 테스트용: GET + PUT만  
setupMockHandlerUpdating(initEvents)

// 삭제 테스트용: GET + DELETE만
setupMockHandlerDeletion(initEvents)

단일 통합 함수 대신 테스트 목적별로 핸들러를 분리했습니다. server.use()로 런타임에 필요한 핸들러만 교체하여 메모리 사용량을 줄이고 테스트 의도를 명확하게 했습니다.

결과

// 병렬 테스트 안전성 검증
test('이벤트 생성', async () => {
  setupMockHandlerCreation([]); // 빈 배열로 시작
  const result = await createEvent(eventData);
  expect(result).toBeDefined(); // 다른 테스트 영향 없이 독립 실행
});

test('이벤트 삭제', async () => {
  setupMockHandlerDeletion([existingEvent]); // 독립적인 초기 데이터
  const result = await deleteEvent(existingEvent.id);
  expect(result.success).toBe(true);
});

각 테스트가 독립적인 데이터 환경을 가지게 되어 병렬 실행 시에도 안정적으로 동작합니다. 테스트 실행 순서에 관계없이 항상 동일한 결과를 보장합니다.

Q. 테스트를 독립적으로 구동시키기 위해 작성했던 설정들을 소개해주세요.

심화 과제

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

과제 셀프회고

기술적 성장

커스텀 훅 테스팅 패턴

renderHook을 통한 훅 독립 테스트

컴포넌트 없이 훅만 독립적으로 실행하고 결과를 검증할 수 있는 개념

const { result } = renderHook(() => useProducts({ products, setProducts }));

act() 함수의 역할

상태 업데이트가 포함된 훅 테스트에서 React 렌더링 사이클과 테스트 환경을 동기화하는 요소

act(() => {
  result.current.addProduct(newProduct);
});

훅 테스트 3단계 패턴

  1. 초기 상태 검증: 훅이 올바른 초기값 반환
  2. 상태 변화 검증: 함수 호출 후 상태 변경 확인
  3. 사이드 이펙트 검증: 외부 의존성(알림, localStorage 등) 호출 확인

코드 품질

App 컴포넌트를 리팩토링했지만, 아직도 비대합니다. props drilling을 최소화 할 수 있도록 리팩토링이 필요하다고 생각합니다.

학습 효과 분석

테스트 구조와 AAA 패턴

AAA 패턴

AAA(Arrange-Act-Assert) 패턴을 일관되게 적용했습니다. 각 단계가 명확히 구분되니 테스트를 읽기 쉬워지고 디버깅할 때도 어느 단계에서 문제가 생겼는지 바로 파악할 수 있었습니다.

const { result } = renderHook(() => useProducts({ products, setProducts }));

describe와 it을 통한 테스트 구조화

관련된 테스트들을 describe로 그룹화하면서 테스트가 체계적으로 구성되었습니다. 특히 중첩된 describe를 사용해 윤년 처리, 에러 케이스 등 로직별로 분류하니 테스트 의도가 훨씬 명확해졌습니다.

describe('getDaysInMonth', () => {
  describe('윤년 처리', () => {
    it('4로 나누어떨어지는 해는 윤년이다', () => {});
    it('100으로 나누어떨어지는 해는 평년이다', () => {});
    it('400으로 나누어떨어지는 해는 윤년이다', () => {});
  });
  
  describe('에러 케이스', () => {
    it('잘못된 월이 입력되면 에러를 던진다', () => {});
  });
});

Assertion 메서드 선택 기준

처음에는 toBe()만 썼다가 객체 비교에서 실패하는 경우를 겪었습니다. toBe는 원시값 비교, toEqual은 객체/배열 내용 비교라는 차이점을 이해하고 나서 상황에 맞는 assertion을 선택할 수 있게 되었습니다.

// 원시값 비교
expect(result).toBe(29);

// 객체/배열 내용 비교  
expect(dateObject).toEqual(expectedDate);

// 배열 포함 여부
expect(products).toContain(newProduct);

// 타입 검증
expect(result).toBeInstanceOf(Date);

리뷰 받고 싶은 내용

1. MSW 핸들러 설계의 적절성

현재 테스트 독립성을 위해 setupMockHandlerCreation, setupMockHandlerUpdating, setupMockHandlerDeletion으로 목적별 핸들러를 분리했습니다. 이런 목적별 분리 접근법이 MSW 모범 사례에 부합하는지 검토해주실 수 있나요? 대안으로 하나의 통합 핸들러에서 테스트 ID별로 데이터를 격리하는 방법도 고려해볼 만한지 의견을 듣고 싶습니다.

2. 하드코딩된 대기 시간의 적절성과 대안

// 비동기 처리 완료까지 대기
await new Promise((resolve) => setTimeout(resolve, 1000));

// 성공 확인 - 이벤트 리스트 또는 성공 메시지
await waitFor(() => {
  const hasEvent = within(eventList).queryByText('새로운 회의');
  const hasSuccessMessage = screen.queryByText('일정이 추가되었습니다.');
  expect(hasEvent || hasSuccessMessage).toBeTruthy();
});

API 호출 완료를 위해 1초 하드코딩 대기 후 waitFor로 재검증하는 패턴을 사용했습니다. 이런 이중 대기 방식이 테스트 신뢰성과 실행 시간 측면에서 효율적인지, 아니면 MSW 응답 완료를 더 정확히 감지할 수 있는 방법이 있는지 피드백 부탁드립니다.

과제 피드백

안녕하세요 지수님! 7주차 과제 잘 진행해주셨네요!! 고생하셨습니다 ㅎㅎ

현재 테스트 독립성을 위해 setupMockHandlerCreation, setupMockHandlerUpdating, setupMockHandlerDeletion으로 목적별 핸들러를 분리했습니다. 이런 목적별 분리 접근법이 MSW 모범 사례에 부합하는지 검토해주실 수 있나요? 대안으로 하나의 통합 핸들러에서 테스트 ID별로 데이터를 격리하는 방법도 고려해볼 만한지 의견을 듣고 싶습니다.

잘 해주셨다고 생각해요! 다만 모범사례가 무엇일까... 에 대한 고민이 있네요 ㅋㅋ 사실 제가 팀에서 MSW를 많이 쓰진 않고 있다보니.. 그래도 똑같은 상황이 벌어진다고 했을 때 지수님께서 만들어주신 것 처럼 만들 것 같아요 ㅎㅎ

하드코딩된 대기 시간의 적절성과 대안

아마 Promise를 통해 wait을 하지 않아도 정상동작 할 것 같아요 ㅎㅎ waitFor이 비슷하게 동작해서요. 대신 기다리는 시간이 더 필요하다면 waitFor의 옵션을 추가하면 된답니다!

https://testing-library.com/docs/dom-testing-library/api-async/#waitfor

function waitFor<T>(
  callback: () => T | Promise<T>,
  options?: {
    container?: HTMLElement
    timeout?: number
    interval?: number
    onTimeout?: (error: Error) => Error
    mutationObserverOptions?: MutationObserverInit
  },
): Promise<T>

timeout을 설정해주면 되어요!