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단계 패턴
- 초기 상태 검증: 훅이 올바른 초기값 반환
- 상태 변화 검증: 함수 호출 후 상태 변경 확인
- 사이드 이펙트 검증: 외부 의존성(알림, 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을 설정해주면 되어요!