⚠️ 문제 상황
<Calendar /> 컴포넌트를 테스팅하는 과정에서 queryClient , recoilRoot 에러 발생
- ERROR 1 : No QueryClient set, use QueryClientProvider to set one
에러난 코드는 다음과 같다.
// _app.tsx
...
return (
<RecoilRoot initializeState={initializer}>
<CustomHead type={`invite`} />
<SocketContextProvider>
<ThemeProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Layout>
<Component {...pageProps} />
</Layout>
<ToastContainer autoClose={2000} pauseOnHover />
</Hydrate>
</QueryClientProvider>
</ThemeProvider>
</SocketContextProvider>
</RecoilRoot>
)
// Calendar.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MainCalendar } from '@/components/project-main/Calendar';
import { renderWithQueryClient } from '../test-utils';
jest.mock('next/router', () => ({
useRouter: jest.fn().mockReturnValue({
query: { channelId: 1, pageId: 'pageId', type: 'type' },
}),
}));
beforeAll(() => {
render(<MainCalendar />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe(`<Calendar />`, () => {
it(`캘린더의 날짜를 클릭하기 전에는 일정 추가 버튼은 비활성 상태이다`, () => {
const addBtn = screen.getByRole('button', { name: '일정 추가' });
expect(addBtn).not.toBeEnabled();
});
it(`캘린더의 날짜를 클릭하면 일정 추가 버튼은 활성화 상태로 바뀌고 내용은 '닫기'로 바뀐다`, () => {
screen.findByText(`07`);
});
});
기존 _app.tsx에서 QueryClientProvider로 component를 감싸서 queryClient를 제공해주고 있었기 때문에,
render(<MainCalendar />로 단순 컴포넌트 렌더시 queryClient가 없어서 발생하는 문제였다.
import { render } from '@testing-library/react';
beforeAll(() => {
render(<MainCalendar />); // queryClient가 제공되지 않음
});
그래서 queryClient를 기본으로 생성하기 위해서 커스텀 함수를 만들고 그 안에서 render함수를 리턴했다.
참고 - https://tkdodo.eu/blog/testing-react-query
// src/test/test-utils/index.tsx
// 컴포넌트를 render하는 커스텀함수
import { render } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactElement } from 'react';
import { RenderResult } from '@testing-library/react';
const generateTestQueryClient = () => {
const client = new QueryClient();
const options = client.getDefaultOptions();
options.queries = { ...options.queries, retry: false };
return client;
};
export const renderWithQueryClient = (
ui: ReactElement,
client?: QueryClient
): RenderResult => {
const queryClient = client ?? generateTestQueryClient();
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
);
};
1. generateTestQueryClient 함수에서 retry: false query 옵션과 함께 queryClient를 생성한다.
2. renderWithQueryClient 함수는 인자로 컴포넌트와, queryClient를 받는데 만약 queryClient가 없다면, generateTestQueryClient 함수를 호출해서 새로운 queryClient를 생성한다.
그리고 인자로 받은 컴포넌트를 <QueryClientProvider />로 감싸서 렌더한다.
결과 : 더 이상 queryClient 에러는 발생하지 않는다!
- ERROR 2 : This component must be used inside a <RecoilRoot> component.
이번엔 RecoilRoot안에 컴포넌트가 있어야 한다고 엄청난 빨간줄과 함께 에러가 발생한다.
마찬가지로 _app.tsx에 Recoil로 전역 상태 관리를 위해 RecoilRoot를 감싸줬지만 testing render시에는 없기 때문에 발생했다.
// _app.tsx
...
return (
<RecoilRoot initializeState={initializer}>
<Component />
</RecoilRoot>
);
구글링 해보니, @testing-library/react-hooks의 renderHook 메서드를 이용해서 커스텀 훅을 테스팅할 수 있다고 했다.
- 시도 1
// Calendar.test.tsx
import { RecoilRoot } from 'recoil';
import { renderHook } from '@testing-library/react-hooks';
...
const { result } = renderHook(
() => useRecoilState(calendarAddScheduleState),
{
wrapper: RecoilRoot
);
<Calendar />에서 atom 상태에 따라 버튼의 내용이 바뀌는 로직이 있었기 때문에 atom을 불러와서 wrapper로 RecoilRoot를 설정해주면 해결 될 줄 알았다.
결과 : 같은 에러 발생.
- 시도 2
atom을 가져왔지만, 초깃값 세팅이 안되는게 아닌가? 하는 생각에 초깃값 세팅을 위해 더 구글링 해봤다.
그래서 찾은 방법이 RecoilRoot의 initializeState 속성을 여기서도 적용 시켜주는 것이였다.
const { result } = renderHook(
() => useRecoilState(calendarAddScheduleState),
{
wrapper: ({ children }: { children: React.ReactElement }) => {
return (
<RecoilRoot
initializeState={(snap) => {
snap.set(calendarAddScheduleState, false);
}}
>
{children}
</RecoilRoot>
);
},
}
);
그리고 다시 yarn test 실행,
결과 : 똑같은 에러 발생..
- 시도 3
wrapper로 RecoilRoot도 전달 해줬고, atom 상태도 전달 해줬는데, 전혀 해결될 기미가 보이지 않았다.
뭔가 슬슬 방향을 잘못 잡은 듯한 느낌이 들기 시작..
recoil을 mocking 한게 아니라서, RecoilRoot만 적용시켜주면 간단하게 해결될 것 같았는데.. 흠..
지금 정말 필요한 건, renderHook으로 커스텀 훅을 테스팅하는게 아니라 단순 RecoilRoot로만 감싸주면 된다는 생각이 들었다.
그래서 혹시?하는 생각에, 아까 queryClient를 생성했던 renderWithQueryClient 함수에 아예 RecoilRoot로 감싸줘 봤다.
// src/test/test-utils/index.tsx
import { RecoilRoot } from 'recoil';
export const renderWithQueryClient = (
ui: ReactElement,
client?: QueryClient
): RenderResult => {
const queryClient = client ?? generateTestQueryClient();
return render(
<RecoilRoot>
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
</RecoilRoot>
);
};
결과 :
이럴수가; 이렇게 간단히 해결되다니..
renderHook 함수의 wrapper로 RecoilRoot를 전달했을 땐 왜 안됐을까 생각해보니,
// Calendar.test.tsx
beforeAll(() => {
renderWithQueryClient(<MainCalendar />);
});
describe(`<Calendar />`, () => {
it(`캘린더의 날짜를 클릭하기 전에는 일정 추가 버튼은 비활성 상태이다`, () => {
const { result } = renderHook(
() => useRecoilState(calendarAddScheduleState),
{
wrapper: RecoilRoot
}
);
const addBtn = screen.getByRole('button', { name: '일정 추가' });
expect(addBtn).not.toBeEnabled();
...
// Calendar.tsx
export const MainCalendar = () => {
...
const scheduleValue = useRecoilValue(calendarScheduleState); // atom 사용 부분
...
}
위 코드처럼 Calendar.test.tsx 파일에서 모든 테스팅에 앞서서 MainCalendar 컴포넌트를 render 시키기 위해 jest의 beforeAll lifecycle을 사용했다.
그러다보니 renderHook의 wrapper로 감싸지는 순간보다 beforeAll로 컴포넌트가 렌더링 되는 순간이 먼저여서
MainCalendar의 내부 atom을 사용하는 부분이 먼저 참조돼서 에러가 나는게 아닐까 싶었다.
💯 문제 해결
react-query의 queryClient 생성해주고, RecoilRoot로 컴포넌트를 미리 감싸주면서 문제 해결~
'FRONTEND > 백만가지 ERROR' 카테고리의 다른 글
EC2 - Nginx-proxy-manager 폴더 permission denied (0) | 2024.01.30 |
---|---|
Nextjs + Docker로 EC2 배포할 때 500Error (0) | 2023.09.14 |
yarn create next-app 설치 안됨 (0) | 2023.06.28 |