🔥 테스트

682자
8분

React Query는 우리가 제공하는 훅이나 그것을 감싸는 사용자 정의 훅을 통해 작동합니다. React 17 이하 버전에서는 이러한 사용자 정의 훅에 대한 단위 테스트를 React Hooks Testing Library를 사용해 작성할 수 있습니다.

다음 명령어로 라이브러리를 설치하세요:

npm install @testing-library/react-hooks react-test-renderer --save-dev
text

(react-test-renderer 라이브러리는 @testing-library/react-hooks의 피어 의존성으로 필요하며, 사용 중인 React 버전과 일치해야 합니다.)

참고: React 18 이상을 사용할 때는 @testing-library/react 패키지를 통해 직접 renderHook을 사용할 수 있으며, @testing-library/react-hooks는 더 이상 필요하지 않습니다.

첫 번째 테스트 작성하기

설치가 완료되면 간단한 테스트를 작성할 수 있습니다. 다음과 같은 사용자 정의 훅이 있다고 가정해 봅시다:

export function useCustomHook() {
  return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })
}
 
typescript

React 17 이하에서는 이에 대한 테스트를 다음과 같이 작성할 수 있습니다:

const queryClient = new QueryClient()
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
 
const { result, waitFor } = renderHook(() => useCustomHook(), { wrapper })
 
await waitFor(() => result.current.isSuccess)
 
expect(result.current.data).toEqual('Hello')
 
typescript

React 18 이상에서는 waitFor의 동작이 변경되어 위의 테스트를 다음과 같이 수정해야 합니다:

import { renderHook, waitFor } from "@testing-library/react";
 
...
 
const { result } = renderHook(() => useCustomHook(), { wrapper });
 
await waitFor(() => expect(result.current.isSuccess).toBe(true));
 
typescript

여기서 QueryClient와 QueryClientProvider를 구축하는 사용자 정의 래퍼를 제공합니다. 이는 테스트를 다른 테스트와 완전히 격리하는 데 도움이 됩니다.

이 래퍼를 한 번만 작성할 수 있지만, 그렇게 하려면 모든 테스트 전에 QueryClient를 초기화하고 테스트가 병렬로 실행되지 않도록 해야 합니다. 그렇지 않으면 한 테스트가 다른 테스트의 결과에 영향을 미칠 수 있습니다.

재시도 기능 끄기

라이브러리는 기본적으로 지수 백오프를 사용하여 세 번의 재시도를 수행합니다. 이는 오류가 발생하는 쿼리를 테스트하려고 할 때 테스트가 시간 초과될 가능성이 높다는 것을 의미합니다. 재시도를 끄는 가장 쉬운 방법은 QueryClientProvider를 통해 설정하는 것입니다. 위의 예제를 확장해 보겠습니다:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ 재시도를 끕니다
      retry: false,
    },
  },
})
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
 
typescript

이렇게 하면 컴포넌트 트리의 모든 쿼리에 대한 기본값을 "재시도 없음"으로 설정합니다. 단, 실제 useQuery에 명시적인 재시도 설정이 없는 경우에만 작동한다는 점을 알아두는 것이 중요합니다. 5번의 재시도를 원하는 쿼리가 있다면 이 설정이 여전히 우선순위를 가집니다. 기본값은 대체용으로만 사용되기 때문입니다.

Jest에서 gcTime을 무한대로 설정하기

Jest를 사용한다면 gcTime을 무한대로 설정하여 "Jest did not exit one second after the test run completed" 오류 메시지를 방지할 수 있습니다. 이는 서버에서의 기본 동작이며, gcTime을 명시적으로 설정하는 경우에만 필요합니다.

네트워크 호출 테스트하기

React Query의 주요 용도는 네트워크 요청을 캐시하는 것이므로, 우리의 코드가 올바른 네트워크 요청을 하고 있는지 테스트하는 것이 중요합니다.

이를 테스트하는 방법은 많지만, 이 예제에서는 nock을 사용하겠습니다.

다음과 같은 사용자 정의 훅이 있다고 가정해 봅시다:

function useFetchData() {
  return useQuery({
    queryKey: ['fetchData'],
    queryFn: () => request('/api/data'),
  })
}
 
typescript

이에 대한 테스트는 다음과 같이 작성할 수 있습니다:

const queryClient = new QueryClient()
const wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
 
const expectation = nock('<http://example.com>').get('/api/data').reply(200, {
  answer: 42,
})
 
const { result, waitFor } = renderHook(() => useFetchData(), { wrapper })
 
await waitFor(() => {
  return result.current.isSuccess
})
 
expect(result.current.data).toEqual({ answer: 42 })
 
typescript

여기서는 waitFor를 사용하여 쿼리 상태가 요청 성공을 나타낼 때까지 기다립니다. 이렇게 하면 훅이 완료되고 올바른 데이터를 가지고 있다는 것을 알 수 있습니다. 참고: React 18을 사용할 때는 위에서 언급한 대로 waitFor의 동작이 변경되었습니다.

더 보기 / 무한 스크롤 테스트하기

먼저 API 응답을 모의해야 합니다:

function generateMockedResponse(page) {
  return {
    page: page,
    items: [...]
  }
}
 
typescript

그런 다음, nock 설정에서 페이지에 따라 응답을 구분해야 하며, 이를 위해 uri를 사용합니다. 여기서 uri의 값은 "/?page=1" 또는 "/?page=2"와 같은 형태가 됩니다:

const expectation = nock('<http://example.com>')
  .persist()
  .query(true)
  .get('/api/data')
  .reply(200, (uri) => {
    const url = new URL(`http://example.com${uri}`)
    const { page } = Object.fromEntries(url.searchParams)
    return generateMockedResponse(page)
  })
 
typescript

(.persist()에 주목하세요. 이 엔드포인트를 여러 번 호출할 것이기 때문입니다)

이제 안전하게 테스트를 실행할 수 있습니다. 여기서 핵심은 데이터 검증이 통과할 때까지 기다리는 것입니다:

const { result, waitFor } = renderHook(() => useInfiniteQueryCustomHook(), {
  wrapper,
})
 
await waitFor(() => result.current.isSuccess)
 
expect(result.current.data.pages).toStrictEqual(generateMockedResponse(1))
 
result.current.fetchNextPage()
 
await waitFor(() =>
  expect(result.current.data.pages).toStrictEqual([
    ...generateMockedResponse(1),
    ...generateMockedResponse(2),
  ]),
)
 
expectation.done()
 
typescript

참고: React 18을 사용할 때는 위에서 언급한 대로 waitFor의 동작이 변경되었습니다.

추가 학습

추가 팁과 mock-service-worker를 사용한 대체 설정에 대해서는 커뮤니티 리소스의 React Query 테스트하기를 참조하세요.