테스트
React Query는 당사가 제공하는 훅이나 이를 감싸는 커스텀 훅을 통해 작동합니다.
React 17 이하에서는 React Hooks Testing Library 라이브러리를 사용하여 이러한 커스텀 훅의 단위 테스트를 작성할 수 있습니다.
다음을 실행하여 설치합니다:
npm install @testing-library/react-hooks react-test-renderer --save-dev
(react-test-renderer 라이브러리는 @testing-library/react-hooks의 peer dependency로 필요하며, 사용 중인 React 버전과 일치해야 합니다.)
참고: React 18 이상을 사용할 때는 renderHook을 @testing-library/react 패키지에서 직접 사용할 수 있으며 @testing-library/react-hooks는 더 이상 필요하지 않습니다.
첫 번째 테스트
설치가 완료되면 간단한 테스트를 작성할 수 있습니다. 다음 custom 훅이 주어졌다고 가정합니다:
export function useCustomHook() {
return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' })
}
이에 대한 테스트를 다음과 같이 작성할 수 있습니다:
import { renderHook, waitFor } from '@testing-library/react'
const queryClient = new QueryClient()
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
const { result } = renderHook(() => useCustomHook(), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual('Hello')
QueryClient와 QueryClientProvider를 빌드하는 사용자 지정 래퍼를 제공한다는 점에 유의하세요. 이를 통해 테스트가 다른 모든 테스트로부터 완전히 격리되도록 할 수 있습니다.
이 래퍼를 한 번만 작성할 수도 있지만, 그렇게 하려면 매 테스트 전에 QueryClient가 지워지는지 확인해야 하며, 한 테스트가 다른 테스트의 결과에 영향을 주지 않도록 테스트를 병렬로 실행해서는 안 됩니다.
재시도 끄기
라이브러리는 기본적으로 지수 백오프를 적용해 세 번 재시도하므로, 오류가 있는 쿼리를 테스트하려는 경우 테스트가 시간 초과될 가능성이 큽니다. 재시도를 끄는 가장 쉬운 방법은 QueryClientProvider를 사용하는 것입니다. 위 예제를 확장해 보겠습니다:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ✅ turns retries off
retry: false,
},
},
})
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
이렇게 하면 컴포넌트 트리의 모든 쿼리에 대한 기본값이 "재시도 없음"으로 설정됩니다. 실제 useQuery에 명시적인 재시도 설정이 없어야만 이 설정이 적용된다는 점을 알아야 합니다. 쿼리가 5회 재시도를 요구한다면 기본값은 대체 설정으로만 사용되므로 해당 설정이 여전히 우선합니다.
Jest에서 gcTime을 Infinity로 설정하기
Jest를 사용하는 경우 "테스트 실행이 완료된 후 1초가 지나도 Jest가 종료되지 않았습니다" 오류 메시지를 방지하려면 gcTime을 Infinity로 설정할 수 있습니다. 이는 서버의 기본 동작이며, gcTime을 명시적으로 설정하는 경우에만 설정해야 합니다.
네트워크 호출 테스트
React Query의 주된 용도는 네트워크 요청을 캐시하는 것이므로, 무엇보다 코드가 올바른 네트워크 요청을 수행하는지 테스트할 수 있어야 합니다.
이를 테스트하는 방법은 많지만, 이 예시에서는 nock을 사용하겠습니다.
다음과 같은 사용자 정의 훅이 있다고 가정합니다:
function useFetchData() {
return useQuery({
queryKey: ['fetchData'],
queryFn: () => request('/api/data'),
})
}
이에 대한 테스트를 다음과 같이 작성할 수 있습니다:
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 } = renderHook(() => useFetchData(), { wrapper })
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(result.current.data).toEqual({ answer: 42 })
여기서는 waitFor를 사용하고 쿼리 상태가 요청 성공을 나타낼 때까지 기다립니다. 이를 통해 훅이 완료되었으며 올바른 데이터를 가지고 있어야 함을 알 수 있습니다. 참고: React 18을 사용할 때는 위에서 설명한 대로 waitFor의 의미 체계가 변경되었습니다.
더 불러오기/무한 스크롤 테스트하기
먼저 API 응답을 모의 처리해야 합니다
function generateMockedResponse(page) {
return {
page: page,
items: [...]
}
}
그런 다음 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)
})
(이 엔드포인트에서 여러 번 호출할 것이므로 .persist()에 유의하세요)
이제 테스트를 안전하게 실행할 수 있으며, 여기서 핵심은 데이터 어설션이 통과할 때까지 기다리는 것입니다:
const { result } = renderHook(() => useInfiniteQueryCustomHook(), {
wrapper,
})
await waitFor(() => expect(result.current.isSuccess).toBe(true))
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()
참고: React 18을 사용할 때는 위에서 설명한 대로 waitFor의 의미 체계가 변경되었습니다.
추가 자료
추가 팁과 mock-service-worker를 사용하는 대체 설정은 TkDodo가 작성한 React Query 테스트 관련 문서를 참조하세요.