인프런 커뮤니티 질문&답변

jack님의 프로필 이미지
jack

작성한 질문수

실무에 바로 적용하는 프런트엔드 테스트 - 1부. 테스트 기초: 단위・통합 테스트

3.3. 리액트 훅 테스트(feat. act 함수)

안녕하세요. 훅 테스트 질문이 있습니다!

해결된 질문

작성

·

228

·

수정됨

0

예제에서 말씀해주신것처럼,

result.current.setState() 를 호출해서 act() 를 통해 업데이트 된 상태를 검증하는 방법을 말씀해주셨는데요.

 

훅 내부 이펙트에서 상태를 업데이트하는 로직을 검증하려면 어떤 식으로 검증해야할까요?

 

export const useDarkMode = (defaultTheme = THEME["LIGHT"]) => {
  const [theme, setTheme] = useState(defaultTheme);

  const changeTheme = (type: keyof typeof THEME) => {
    setTheme(THEME[type]);
  };

  useLayoutEffect(() => {
    const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");

    const changeListener = ({ matches }: MediaQueryListEvent) => {
      setTheme(THEME[matches ? "DARK" : "LIGHT"]);
    };

    mediaQueryList.addEventListener("change", changeListener);

    return () => {
      mediaQueryList.removeEventListener("change", changeListener);
    };
  }, []);

  return {
    theme,
    changeTheme,
  };
};

 

위 useDarkMode() 훅 내부 useLayoutEffect() 에서

window.matchMedia 의 change 이벤트를 감지하면, setTheme() 하도록 설계되어 있는데요.

 

window.matchMedia 함수의 matches 결과를 true 로 모킹하고,

window.matchMedia.dispatchEvent('change') 를 일으켜 검증을 시도해보았는데요.

생각처럼 검증이 되지 않는 것 같습니다.ㅠ

 

혹시 이렇게 검증을 시도하는 것이 맞는지. 검증하는게 맞는지. 여쭤봐도 될까요? 감사합니다.

답변 2

1

jack님의 프로필 이미지
jack
질문자

자세히 설명해주셔서 감사합니다. 너무나 잘 이해되었습니다!

말씀해주신대로 스토리북을 활용하는 방법이 더 깔끔하다고 생각이 되어지네요.

좋은 강의에 항상 감사드립니다 🙂

1

코드 조커, 오프님의 프로필 이미지
코드 조커, 오프
지식공유자

안녕하세요 jack님!

우선 보통 effect내에서 상태를 직접 변경하는 경우(effect 내에서 setTheme 직접 호출)에는 별도 작업없이 result.current.theme만 체크해도 변경된 상태값을 기준으로 검증할 수 있는대요.

하지만 말씀하신 코드에서는 effect 함수 내에서 mediaQueryListchange 이벤트 등록 리스너만 실행됩니다. 그리고 이후에 change 이벤트가 발생했을때만 theme의 상태가 변경되도록 작성되어 있습니다.

이런 경우 원하는 matches 값 기준으로 이벤트 리스너를 등록하고 실행시켜야 하기 때문에 아래 정도로 window.matchMedia의 구현 자체에 모킹이 필요합니다.

const mockMatchMedia = matches => {
  let targetListener;
  // 이벤트 등록 핸들러 스텁
  const stubAddEventListener = (eventName, listener) => {
    targetListener = () => listener({ matches });
  };

  // 이벤트 제거 핸들러 스텁
  const stubRemoveEventListener = () => {
    targetListener = null;
  };

  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: vi.fn().mockImplementation(query => ({
      matches,
      media: query,
      onchange: null,
      addListener: vi.fn(), // deprecated
      removeListener: vi.fn(), // deprecated
      addEventListener: stubAddEventListener,
      removeEventListener: stubRemoveEventListener,
      dispatchEvent: vi.fn(),
    })),
  });
  // 이벤트 핸들러 강제 실행을 위한 dispatchEvent
  return { dispatchEvent: () => targetListener() };
};

// window.matchMedia에 대한 구현을 이전 설정으로 되돌린다.
afterEach(() => {
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: vi.fn().mockImplementation(query => ({
      matches: false,
      media: query,
      onchange: null,
      addListener: vi.fn(), // deprecated
      removeListener: vi.fn(), // deprecated
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    })),
  });
});

it('theme가 dark 가 된다.', () => {
  const { dispatchEvent } = mockMatchMedia(true);
  const { result } = renderHook(() => useDarkMode());

  act(() => {
    dispatchEvent();
  });

  expect(result.current.theme).toBe('dark');
});

위 코드처럼 window.matchMedia 함수의 메커니즘에 필요한 이벤트 관련 API들을 모두모킹해야 비로소 원하는 시나리오대로 테스트를 작성할 수 있습니다.

(node.js 환경에서는 미디어 쿼리를 전혀 지원하지 않기 때문에 이렇게 원하는 시나리오를 검증할 수 있는 별도 구현체를 만들어야만 확인이 가능합니다.)

개인적으로 단위 테스트에서 이렇게 미디어 쿼리와 관련된 모듈 모킹을 통해 테스트하기보다는.. 스토리북내에서 실제 브라우저의 미디어 쿼리와 연동하여 useDarkModetheme 상태가 제대로 반영되는지 확인하는 것이 더 정확하고 깔끔한 방법일 것 같습니다..!

혹시 더 궁금하거나 다른 의견이 있으시면 얼마든지 답글 남겨주세요.

감사합니다 🙂

jack님의 프로필 이미지
jack

작성한 질문수

질문하기