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 이상을 사용하는 경우
@testing-library/react
패키지를 통해renderHook
을 직접 사용할 수 있으며,@testing-library/react-hooks
는 더 이상 필요하지 않습니다."
설치가 완료되면 간단한 테스트를 작성할 수 있습니다. 다음 커스텀 훅이 주어집니다:
export function useCustomHook() {
return useQuery({ queryKey: ['customHook'], queryFn: () => 'Hello' });
}
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");
React 18 이상에서는 waitFor
의 의미가 변경되었으므로 위의 테스트는 다음과 같이 수정해야 합니다:
import { renderHook, waitFor } from "@testing-library/react";
...
const { result } = renderHook(() => useCustomHook(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
React Query는 QueryClient
, QueryClientProvider
를 빌드하는 커스텀 래퍼를 제공한다는 점에 유의하세요. 이렇게 하면 테스트가 다른 테스트와 완전히 격리되도록 할 수 있습니다.
이 래퍼를 한 번만 작성할 수도 있지만, 그렇게 할 경우 모든 테스트 전에 QueryClient
를 지우고 테스트가 병렬로 실행되지 않도록 해야 하며, 그렇지 않으면 한 테스트가 다른 테스트의 결과에 영향을 미칠 수 있습니다.
라이브러리는 기본적으로 지수 백오프(exponential backoff)를 사용하여 세 번 재시도하도록 설정되어 있으므로 잘못된 쿼리를 테스트하려는 경우 테스트가 시간 초과될 수 있습니다. 재시도를 끄는 가장 쉬운 방법은 QueryClientProvider를 사용하는 것입니다. 위의 예제를 확장해 보겠습니다:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ✅ turns retries off
retry: false,
},
},
})
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
이렇게 하면 컴포넌트 트리의 모든 쿼리에 대한 기본값이 "no retries"로 설정됩니다. 이 설정은 실제 useQuery
에 명시적인 재시도가 설정되어 있지 않은 경우에만 작동한합니다. 5번의 재시도를 원하는 쿼리가 있는 경우 기본값은 대체(fallback)로만 사용되므로 이 쿼리가 여전히 우선합니다.
When testing we want to suppress network errors being logged to the console. To do this, we can pass a custom logger to QueryClient:
테스트할 때 콘솔에 기록되는 네트워크 오류를 억제하고 싶을 수 있습니다. 이를 위해 커스텀 로거를 QueryClient
에 전달하면 됩니다:
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
logger: {
log: console.log,
warn: console.warn,
// ✅ no more errors on the console for tests
error: process.env.NODE_ENV === 'test' ? () => {} : console.error,
},
})
Jest를 사용하는 경우 cacheTime
을 Infinity
로 설정하여 "Jest did not exit one second after the test run completed" 오류 메시지를 방지할 수 있습니다. 이는 서버의 기본 동작이며, 명시적으로 cacheTime
을 설정하는 경우에만 설정하면 됩니다.
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, waitFor } = renderHook(() => useFetchData(), { wrapper });
await waitFor(() => {
return result.current.isSuccess;
});
expect(result.current.data).toEqual({answer: 42});
여기서는 waitFor
를 사용하고 쿼리 상태가 요청이 성공했음을 나타낼 때까지 기다리고 있습니다. 이렇게 하면 훅이 완료되었고 올바른 데이터를 가져야 한다는 것을 알 수 있습니다. 참고: React 18을 사용할 때는 위에서 언급한 것처럼 waitFor
의 의미가 변경되었습니다.
먼저 API 응답을 모방(mock)해야 합니다.
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()
에 주목하세요).
이제 테스트를 안전하게 실행할 수 있습니다. 여기서 요령은 데이터 assertion이 통과될 때까지 await하는 것입니다:
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();
참고: React 18을 사용할 때, 위에서 언급한 것처럼 waitFor
의 의미가 변경되었습니다.
추가 팁과 mock-service-worker
를 사용한 대체 설정에 대해서는 커뮤니티 리소스에서 Testing React Query를 참조하세요.
Reference