오늘 글에서는 React Query hooks를 테스팅하고 해당 hooks를 사용하는 컴포넌트까지 테스팅 해보려 합니다.
프로젝트 준비
테스팅을 위해 필요한 라이브러리를 설치해보겠습니다.
yarn add @tanstack/react-query axios
yarn add -D jest jest-environment-jsdom msw @testing-library/react @testing-library/jest-dom
npx msw init public/
라이브러리 설치 후, 아래 명령어를 입력해주시면 자동으로 public 폴더에 mockServiceWorker.js 파일이 생성됩니다.
// pages/_app.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { AppProps } from "next/app";
const queryClient = new QueryClient()
function MyApp({ Component, pageProps }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
export default MyApp;
제 프로젝트는 next.js를 사용하기 때문에 _app.tsx파일에 React Query Provider를 선언해주었으며 Create React App을 사용하시는 분들은 App.tsx 파일에 Provider를 선언해주시면 됩니다.
msw 코드 작성
msw는 Service Worker API를 사용해서 모든 요청을 가로채는 백엔드 API Mocking 라이브러리입니다.
실제 네트워크 요청을 보내며 가짜지만 프로덕션에 가까운 백엔드 코드를 사용해볼 수 있습니다.
msw를 처음 접하시거나 next.js 프로젝트 적용법을 보고 싶으시다면 아래 글을 참고해주세요!
Next.js) msw로 백엔드 Mocking 해보기
msw는 Service Worker API를 사용해서 모든 요청을 가로채는 백엔드 API Mocking 라이브러리입니다. REST API와 GraphQL 모두 지원하며 엔드포인트가 준비되지 않았을 때, 인터넷 연결이 느리거나 존재하지 않
ekard.tistory.com
먼저 프로젝트 루트에 mocks 폴더를 생성합니다. 그 다음, server.ts 파일을 만들고 작성해보겠습니다.
// mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
import "@testing-library/jest-dom";
export const server = setupServer(...handlers);
// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
// Clean up after the tests are finished.
afterAll(() => server.close());
msw가 node.js 환경에서 테스팅 하기 위한 파일입니다. @testing-library/jest-dom을 사용하며
- beforeAll(() => server.listen()): 모든 테스팅 전에 api mocking을 설정해줍니다.
- afterEach(() => server.resetHandlers()): 테스팅 중에 추가 될 수 있는 모든 요청 핸들러들을 리셋합니다.
- afterAll(() => server.close()): 테스팅이 끝난 후 뒷정리를 합니다. useEffect의 return 함수와 목적은 유사합니다.
handlers.ts 파일을 작성하기전에 먼저 Type 파일을 하나 작성해주겠습니다.
// mocks/type.ts
export interface Review {
id: string;
author: string;
text: string;
}
// mocks/handlers.ts
import { rest } from "msw";
import { Review } from "./type";
export const handlers = [
rest.get("/reviews", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<Review[]>([
{
id: "1",
author: "marol",
text: "good vibes only",
},
])
);
}),
];
handlers.ts 파일은 우리가 mocking 하는 백엔드 코드 입니다. express나 AWS Lambda 같은 코드를 작성해보신 분이라면 꽤 낯익은 코드일 것 입니다.
React Query Client 코드 작성
이제 React Query Client 관련 코드를 작성해보겠습니다. mocks 폴더에 util.tsx 파일을 하나 만들어주세요.
createTestQueryClient
// mocks/utils.tsx
import { QueryClient } from "@tanstack/react-query";
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
},
});
React Query를 사용하는 프로젝트는 App.tsx 파일이나 Next.js라면 _app.tsx 파일에 QueryClientProvider를 작성합니다.
하지만 테스팅에서는 테스팅만의 설정이 많기 때문에 별도의 QueryClient를 사용합니다.
또한 새로 작성한 QueryClient는 retry: false 설정으로 쿼리의 재시도를 완전히 막았습니다. 그 이유는 React Query의 exponential backoff 기능 때문인데요. 이 기능은 쿼리를 기본적으로 3번 재시도 합니다. 재시도가 3번 수행되면 잘못된 쿼리를 테스팅 할 때 시간초과가 발생할 수 있습니다.
logger 객체를 살펴보시면 error에 빈 화살표 함수를 덮어씌었습니다.
이유는 콘솔에 에러를 출력하는 기능을 막기 위함입니다. 만약에 작성한 테스팅 코드가 정상적으로 통과되어도 콘솔에는 에러가 띄워져서 여러분에게 혼란을 초래 할 수 있습니다.
renderWithClient
// mocks/utils.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactElement } from "react";
import { render } from "@testing-library/react";
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
},
});
// React Query를 사용하는 컴포넌트를 테스팅하기 위한 함수
export function renderWithClient(ui: ReactElement) {
const testQueryClient = createTestQueryClient();
const { rerender, ...result } = render(
<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
);
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={testQueryClient}>
{rerenderUi}
</QueryClientProvider>
),
};
}
보통 리액트에서 컴포넌트를 테스팅할땐 아래와 같은 코드를 사용합니다.
import { render } from "@testing-library/react";
render(<Reviews />);
이번 글에서는 @testing-library/react의 render 함수를 React Query와 사용하기 위해 renderWithClient 함수를 작성하였습니다. rerender 함수는 렌더링 된 컴포넌트의 props를 업데이트 할 때 사용됩니다.
createWrapper
// mocks/utils.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactElement, ReactNode } from "react";
import { render } from "@testing-library/react";
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {},
},
});
export function renderWithClient(ui: ReactElement) {
const testQueryClient = createTestQueryClient();
const { rerender, ...result } = render(
<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>
);
return {
...result,
rerender: (rerenderUi: ReactElement) =>
rerender(
<QueryClientProvider client={testQueryClient}>
{rerenderUi}
</QueryClientProvider>
),
};
}
//React Query Hooks를 테스팅하기 위한 함수
export function createWrapper() {
const testQueryClient = createTestQueryClient();
// eslint-disable-next-line react/display-name
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={testQueryClient}>
{children}
</QueryClientProvider>
);
}
이번에는 컴포넌트가 아닌 React Query Hooks를 테스팅하기 위해 createWrapper 함수를 작성하였습니다.
작성한 함수는 renderHook 옵션의 wrapper로 전달되어 React Query Hooks를 감싸주게 됩니다.
Redux나 React Query의 Provider와 같은 개념입니다.
테스팅 코드 작성
먼저 React Query Hooks의 테스팅 코드를 작성한 뒤, 실제 사용되는 Hooks를 작성해보겠습니다.
Hooks 테스팅
// __tests__/book/Queries.test.tsx
import { renderHook, waitFor } from "@testing-library/react";
import { createWrapper } from "../../mocks/utils";
import { server } from "../../mocks/server";
import { rest } from "msw";
import { useReviews } from "../../features/book/Queries";
import "@testing-library/jest-dom";
describe("query reviews hook", () => {
test("successful reviews query hook", async () => {
const { result } = renderHook(() => useReviews(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual([
{
author: "marol",
id: "1",
text: "good vibes only",
},
]);
});
test("failure reviews query hook", async () => {
server.use(
rest.get("*", (req, res, ctx) => {
return res(ctx.status(500));
})
);
const { result } = renderHook(() => useReviews(), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.isError).toBe(true));
expect(result.current.error).toBeDefined();
});
});
// features/book/Queries.tsx
import { useQuery } from "@tanstack/react-query";
import { Review } from "../../mocks/type";
import axios from "axios";
const fetchReviews = async () => {
const { data } = await axios.get<Review[]>("/reviews");
return data;
};
export function useReviews() {
return useQuery({
queryKey: ["reviews"],
queryFn: fetchReviews,
});
}
@testing-library/react에도 13.1.0 버전 이후로 Hooks를 테스팅하기 위한 renderHook 함수가 추가 되었습니다.
위에서 작성한 handlers.ts의 백엔드 코드를 기반으로 데이터를 받아 테스팅하는 코드입니다.
await waitFor(() => expect(result.current.isSuccess).toBe(true));
첫번째 테스트에서 위 코드는 React Query가 비동기로 사용되어야하기 때문에 await을 주었으며 React Query 코드의 isSuccess가 true가 반환될 때까지 기다립니다.
true가 되면 이제 React Query의 data를 사용할 수 있기 때문에 결과 값이 mocking 백엔드에 요청한 배열 객체 값과 같은지 확인합니다.
두번째 테스트는 rest.get에서 반드시 status 500(내부 서버 오류)를 반환합니다. 그 뒤에 Hooks의 isError가 true가 되고 결과에 error가 정의되어 있다면 통과되는 테스트입니다.
컴포넌트 테스팅
// __tests__/book/Reviews.test.tsx
import Reviews from "../../features/book/Reviews";
import { server } from "../../mocks/server";
import { renderWithClient } from "../../mocks/utils";
import { rest } from "msw";
import "@testing-library/jest-dom";
describe("query Reviews component", () => {
test("successful reviews query component", async () => {
const result = renderWithClient(<Reviews />);
expect(await result.findByText(/good vibes only/i)).toBeInTheDocument();
});
test("failure reviews query component", async () => {
server.use(
rest.get("*", (req, res, ctx) => {
return res(ctx.status(500));
})
);
const result = renderWithClient(<Reviews />);
expect(
await result.findByText(/에러가 발생하였습니다/)
).toBeInTheDocument();
});
});
// features/book/Reviews.tsx
import { useReviews } from "../../features/book/Queries";
const Reviews = () => {
const { isLoading, error, isFetching, data: reviews } = useReviews();
if (isLoading) return <div>로딩 중...</div>;
if (error instanceof Error)
return <div>에러가 발생하였습니다: {error.message}</div>;
return (
<>
<div>
{reviews && (
<ul>
{reviews.map((review) => (
<li key={review.id}>
<p>{review.text}</p>
<p>{review.author}</p>
</li>
))}
</ul>
)}
<div>{isFetching ? "업데이트 중..." : ""}</div>
</div>
</>
);
};
export default Reviews;
화면에 백엔드에 요청한 결과 값이 제대로 출력되어있는지 확인하는 테스팅 코드입니다.
renderWithClient 함수를 사용하여 React Query를 컴포넌트 테스팅에 사용할 수 있도록 하였습니다.
밑의 에러 테스팅 코드는 이전 예제인 Hooks 테스팅 코드와 유사합니다.
이번 글은 여기까지입니다. 긴 글 읽어주셔서 감사합니다.
Reference:
https://github.com/TkDodo/testing-react-query
GitHub - TkDodo/testing-react-query
Contribute to TkDodo/testing-react-query development by creating an account on GitHub.
github.com
https://tkdodo.eu/blog/testing-react-query
Testing React Query
Let's take a look at how to efficiently test custom useQuery hooks and components using them.
tkdodo.eu
'Development > React' 카테고리의 다른 글
Storybook 7.0을 통한 프로젝트 문서화와 테스팅 (0) | 2023.04.13 |
---|---|
Styled Components ttf 폰트 적용 문제 해결 (0) | 2023.01.30 |
Next.js) msw로 백엔드 Mocking 해보기 (0) | 2023.01.12 |
Next.js)@testing-library로 컴포넌트 테스팅하기 (0) | 2023.01.11 |
React) useState에서 놓치기 쉬운 사실들 (3) (0) | 2022.12.26 |