인프런 커뮤니티 질문&답변

leo.kang님의 프로필 이미지
leo.kang

작성한 질문수

실무에 바로 적용하는 프런트엔드 테스트 - 1부. 테스트 기초: 단위・통합 테스트

4.6. RTL 비동기 유틸 함수를 통한 노출 테스트 작성

show more 버튼이 노출되지 않는 테스트 케이스에서 limit 오동작 문제

해결된 질문

작성

·

178

·

수정됨

1

안녕하세요, 좋은 강의 잘 듣고 있습니다.

보여줄 상품 리스트가 없는 경우 show more 버튼이 노출되지 않는다. 테스트 케이스에서 limit 를 20 이하로 입력해도 테스트 실패로 잡히지 않는 문제가 있는 것 같아 보이는데요..!

원인을 찾아보려 조금 테스트하다 보니.. render -> screen.findAllByTestId 를 하면서 api가 두 번 호출되는 것 같습니다. 그 과정에서 offset이 limit 만큼 증가해 호출되고 있어요. (즉, 두번째 페이지까지 렌더링했을 때를 기준으로 테스트가 돌아가는 것 같습니다)

it('보여줄 상품 리스트가 없는 경우 show more 버튼이 노출되지 않는다.', async () => {
  await render(<ProductList limit={2} />);
  // offset : 0

  await screen.findAllByTestId('product-card');
  // offset : 2
});

offset 이 useInfiniteQuery에서 리턴해주는 pageParams로 인해 만들어지는 것 같아서 pageParams가 원인인 것 같긴 한데.. 해결방법을 모르겠네요.

혹시 이 부분 수정이 어떻게 되면 좋을지 확인 부탁드립니다..!

 

+)

테스트하다가 발견한 건데요, apiRoutes.products 를 mocking하는 handler에서 lastPage로 리턴하는 조건이 잘못된 건 아닌가 해서요..!

ctx.json({ products, lastPage: products.length < limit });

이면 테스트 코드에서 limit을 10으로 준다고 해도, 두 번째 페이지에서 lastPage가 false인 것 같습니다.

ctx.json({ products, lastPage: data.products.length <= offset + limit });

으로 수정되어야 하지 않을까 조심스럽게 제안드려 봅니다..!

답변 1

0

코드 조커, 오프님의 프로필 이미지
코드 조커, 오프
지식공유자

안녕하세요 leo.kang 님!

제가 결혼식에 가야하는 일정때문에 조금 서둘러 답변 드리는 점 양해 부탁드립니다 🥲

저녁에 돌아와서 조금 더 궁금하신 점 남겨주시면 내용 보충해서 남겨두겠습니다.

우선 첫 번째로 남겨주신 질문은 useInfiniteQuery의 동작 문제가 맞는 것 같습니다.

해당 코드를 only를 붙여 돌리거나 순서를 바꿔 가장 먼저 호출하게 하면, 정상적으로 동작하는데요.

구현 부를 조금 살펴보면, OFFSET을 제외한 params를 키로 사용하다보니 캐싱이 되어 이전 요청을 그대로 사용해 해당 문제가 발생하는 것으로 보입니다. params를 options로 변경하면 코드가 테스트가 동작될 겁니다.

사실 offset이 앱 자체에서 변경될 일이 없다보니..저렇게 했던것 같기도 한데요. 저도 사실 요즘 RQ를 사용할 때 선호하는 구현 방식은 아니라 올바른 방법으로 작성한다면, OFFSET도 포함을 시켜줘야 할 것으로 보입니다. 시간이 된다면 최신 RQ를 적용하면서 키 관리 방식을 수정하고 싶네요 ㅎㅎ.. 해당 부분 코드 수정해서 업로드 해 두도록 하겠습니다. 발견해주셔서 감사합니다. 👍

export const useLoadMore = ({ url, options }) => {
  const { params, ...others } = options;
  const context = useInfiniteQuery(
    [url, params],
    ({ queryKey, pageParam = 0 }) =>
      defaultFetcher({
        queryKey,
        pageParam: { offset: pageParam * others.limit, limit: others.limit },
      }),
    {
      getNextPageParam: (lastPage, pages) => {
        return lastPage.lastPage ? false : pages.length;
      },
    },
  );

  return context;
};

두번째로 msw 핸들러 부분이 말씀해주신 부분대로 수정이 되어야 하는 것 같습니다. 이 부분은 명백하게 잘못 구현되어 있는 것 같은데요. 이 부분은 제가 최대한 빠르게 예제 코드에 반영해두도록 하겠습니다. 혼란 드려 죄송합니다.

 

제보 감사하며, 추가로 궁금한 점 있으시면 편하게 댓글 주세요!

leo.kang님의 프로필 이미지
leo.kang
질문자

답변 감사합니다!

그런데 좀 더 테스트 해 보니.. 단순히 queryKey의 문제라기보단, 테스트 케이스가 다른 테스트 케이스에 영향을 주면서 발생하는 문제같아 보입니다.

테스트로 fetcher 쪽에 아래와 같이 콘솔을 찍어서 확인해보면, offset이 계속해서 증가하는 걸 확인할 수 있었어요.

export const defaultFetcher = ({ queryKey, pageParam }) => {
  const [url, params] = queryKey;

  // for test
  console.log('pageParam', pageParam);

  return api.get(url, { params: { ...params, ...pageParam } }).then(res => {
    return res.data;
  });
};

아래와 같이 render.jsx 쪽에 queryClient가 render를 할 때마다 생성되는 것이 아니라, 하나의 인스턴스만 생성되고 그 인스턴스를 매 테스트 케이스마다 사용하게 되는데요, 이 부분이 문제이지 않을까 싶습니다..! queryKey 캐싱의 문제도 이 때문인 것 같아요. (params -> options로 바꿀 필요가 없어 보입니다.)

// https://tanstack.com/query/v4/docs/react/guides/testing
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // ✅ turns retries off
      retry: false
    },
  },
  logger: {
    log: console.log,
    warn: console.warn,
    // ✅ no more errors on the console for tests
    error: process.env.NODE_ENV === 'test' ? () => {} : console.error,
  },
});

export default async (component, options = {}) => {
  const { routerProps } = options;
  const user = userEvent.setup();

  return {
    user,
    ...render(
      <QueryClientProvider client={queryClient}>
        <MemoryRouter {...routerProps}>{component}</MemoryRouter>
        <Toaster />
      </QueryClientProvider>,
    ),
  };
};

queryClient 인스턴스를 매 테스트 케이스마다 독립적으로 사용할 수 있도록

export default async (component, options = {}) => {
  const { routerProps } = options;
  const user = userEvent.setup();

  // https://tanstack.com/query/v4/docs/react/guides/testing
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        // ✅ turns retries off
        retry: false,
      },
    },
    logger: {
      log: console.log,
      warn: console.warn,
      // ✅ no more errors on the console for tests
      error: process.env.NODE_ENV === 'test' ? () => {} : console.error,
    },
  });

  return {
    user,
    ...render(
      <QueryClientProvider client={queryClient}>
        <MemoryRouter {...routerProps}>{component}</MemoryRouter>
        <Toaster />
      </QueryClientProvider>,
    ),
  };
};

으로 변경해주면 해결되는 것 같습니다..!

문서에서도 각 테스트 케이스 별로 독립적인 queryClient가 적용될 수 있도록 하라는 것 같습니다!

코드 조커, 오프님의 프로필 이미지
코드 조커, 오프
지식공유자

안녕하세요 leo.kang님!

훨씬 더 근본적인 정확한 해결방법인 것 같네요. 👍

오후에 저도 조금만 더 증상 살펴본다음에 말씀해주신 내용대로 코드 반영해서 수정해두도록 하겠습니다.

오랜만에 주말에 일정이 많다보니... 미리 찾아보고 말씀드렸어야 했는데 찾아주셔서 감사합니다.

좋은 하루 되세요!

코드 조커, 오프님의 프로필 이미지
코드 조커, 오프
지식공유자

조금 늦었네요!

https://github.com/practical-fe-testing/test-example-shopping-mall/commit/df725bdc3025949a0ae06ced1907e63071ccc659

수정해서 반영해뒀습니다. 늘 감사합니다!

leo.kang님의 프로필 이미지
leo.kang
질문자

확인했습니다, 빠르게 반영해 주셔서 감사합니다!

leo.kang님의 프로필 이미지
leo.kang

작성한 질문수

질문하기