notion image
테스트코드 는 실제 서비스에서 발생할 수 있는 에러나, 유지보수, 신규 개발에서 발생할 수 있는 에러를 사전에 방지할 수 있습니다. [React / TS 도메인 로직 테스트 코드] 게시글을 읽고오시면 FrontEnd 에서 비즈니스 로직의 테스트코드가 어떤 효과를 줄 수 있는지 확인할 수 있습니다.
오늘은 실무에서 많이 사용하는 react-query 는 어떻게 테스팅 할 수 있고, 어떻게 목업 설정을 할 수 있는지 테스트코드 작성의 실제 예제를 확인해보려고 합니다. 따라서 react-query 를 안다는 가정하에 설치법, 세팅(provider)등에 대한 내용은 생략하도록 하겠습니다.

프론트 퍼블리싱

TypeScript
"use client";
import Image from "next/image";

import photoQuery from "@/apis/query/photos.query";

const PhotoItem = ({ photo }: { photo: Photo }) => {
  return (
    <div data-testid="photo">
      <Image src={photo.download_url} alt={photo.url} width={300} height={300} />
      <span>{photo.author}</span>
    </div>
  );
};

const Photos = () => {
  const { data: photos } = photoQuery.getRandomPhotosQuery();

  return (
    <div className="max-w-3xl grid grid-cols-3 gap-3">
      {photos?.map((photo) => (
        <PhotoItem key={photo.id} photo={photo} />
      ))}
    </div>
  );
};

export default Photos;
우리는 위와 같은 퍼블리싱 코드를 하나 작성했습니다. 위의 코드가 보여줄 화면은 매우 간단하게 화면에 api fetch를 통해서 받은 사진 리스트를 grid 형태로 보여주게 될 것입니다.
여기서 api fetch 받는 부분을 react-query 를 통해 캐싱하려고 합니다. (ps. 위와 같은 방식은 react에서 주로 사용하며 next.js에서도 간간히 나오는 패턴이지만 next.js에서는 잘 사용하지는 않습니다.)
우리는 실질적인 api fetch를 수행하는 함수 하나와 api fetch 받은걸 cache하기 위한 react-query 함수 이렇게 2가지 레어어를 구성할 것입니다.

API, Query 로직 구현

TypeScript
// photos.api.ts
import { setSearchParams } from "@/utils/setSearchParams";

const baseUrl = "https://picsum.photos/v2";

const getRandomPhotos = async (page: string = "1", limit: string = "10"): Promise<Photo[]> => {
  const params = { page, limit };
  const url = setSearchParams(`${baseUrl}/list`, params);

  const response = await fetch(url);
  return response.json();
};

export default {
  getRandomPhotos,
};

// photos.query.ts
import { UseQueryResult, useQuery } from "react-query";
import photoApi from "../photos.api";

const getRandomPhotosQuery = (page: string = "1", limit: string = "10"): UseQueryResult<Photo[]> => {
  return useQuery({
    queryKey: ["photos", page, limit],
    queryFn: async () => photoApi.getRandomPhotos(),
    refetchOnWindowFocus: true,
  });
};

export default { getRandomPhotosQuery };
위 처럼 구성하면 우리는 qeuryFn 에서 fetch 받은 것을 토대로 정제할 수 있습니다. 주로 비즈니스 로직이 될 수 있죠!
여기서 주는 return 값은 useQueryResult로 isLoading, isFetching, data, refetch등을 포함하고 있고 우리는 오늘 여기서 data에 해당하는 값을 테스팅할 것입니다.
photos.query.ts 를 통해서 query 함수를 만들었으니 실 코드에서는 photoQuery.getRandomPhotosQuery() 를 통해 호출할 수 있습니다.
호출받은 데이터를 바탕으로 그린 화면은 아래와 같습니다.
notion image
정확히 3개의 같은 크기 그리드가 생성되었고, 이미지가 보여지는 것을 확인할 수 있습니다.
 
우리가 테스트 할 내용은 총 3가지입니다.
  1. 간단한 타이틀 테스트
  1. React-Query의 로딩 상태 목업
  1. React-Query의 실제 Data 목업

Let’s Go Test!

Test Provider 세팅

TypeScript
const renderProviderComponent = ({
  children,
  client,
}: {
  children: JSX.Element;
  client?: QueryClient;
}): React.ReactElement => {
	return (
		<QueryClientProvider client={client ?? new QueryClient()}>{children}</QueryClientProvider>
	)
}
react-query 를 사용하려면 provider로 감싸져야 사용할 수 있으므로 테스트 환경도 동일하게 세팅해줍니다. client 를 외부에서 주입시켜주는데, 굳이 react-query를 테스트할 계획이 아니라면 넘길 수 있습니다.
이제 환경 설정은 끝이났고, 테스트환경에 사용할 mockData를 하나 만들어야 합니다.

MockData 만들기

TypeScript
const mockData = [
  {
    id: "0",
    author: "Alejandro Escamilla",
    width: 5000,
    height: 3333,
    url: "https://unsplash.com/photos/yC-Yzbqy7PY",
    download_url: "https://picsum.photos/id/0/5000/3333",
  },
	...생략
  {
    id: "9",
    author: "Alejandro Escamilla",
    width: 5000,
    height: 3269,
    url: "https://unsplash.com/photos/ABDTiLqDhJA",
    download_url: "https://picsum.photos/id/9/5000/3269",
  },
];
MockData는 실제로 PhotosQuery 가 return 해주는 타입과 동일하게 구현해야 합니다. 만약, 테스트하는 대상이 특정 key(id, author 등)만 사용한다면 사용하는 키만 넣고 타입을 강제로 지정해줘도 무방합니다.

React-Query 테스트하기

TypeScript
describe("<Home /> 테스트", () => {
	// Test1
  it("타이틀 테스트", () => {
    const { getByRole } = render(renderProviderComponent({ children: <Home /> }));

    const title = getByRole("heading", {
      level: 2,
    });

    expect(title).toHaveTextContent("랜덤 사진");
  });
	// Test2
  it("로딩 상태 확인", () => {
    photoQuery.getRandomPhotosQuery = jest.fn().mockReturnValue({
      isLoading: true,
    });

    const client = new QueryClient();

    const { getByText } = render(renderProviderComponent({ children: <Home />, client }));

    expect(getByText("loading")).toBeInTheDocument();
  });
	// Test3
  it("10개가 맞는지.", () => {
    photoQuery.getRandomPhotosQuery = jest.fn().mockReturnValue({
      data: mockData,
    });

    const client = new QueryClient();

    const { getAllByTestId } = render(renderProviderComponent({ children: <Home />, client }));

    expect(getAllByTestId("photo")).toHaveLength(mockData.length);
  });
});
Test1은 간단하게 타이틀을 체크합니다. UI 테스트도 중요하지만, UI의 경우 요구사항에 따라 자주 바뀌곤 합니다. 만약 비즈니스 로직과 뷰 로직, UI 중에 시간에 따라 테스트 우선순위를 지정해야한다면 비즈니스 로직 테스트에 많은 시간을 사용하는 것을 추천합니다.
 
Test2는 로딩 상태를 확인합니다. 저기서 photoQuery.getRandomPhotosQuery 는 뭘까요? 우리가 위에서 지정한 react-query 의 useQuery 를 return 해주는 함수입니다. 우리는 저 photoQuery.getRandomPhotosQuery 에 jest.fn의 mockReturnValue를 통해 목업 데이터를 넣어줄 겁니다.
만약 저렇게 export default로 module를 export 하는게 아니라 fn를 return 하는 경우라면 jest.fn(function)을 통해서도 목업 데이터를 주입할 수 있습니다.
Test2에 isLoading을 넣으면 맨 위에서 작성한 UI는 loading이라는 text를 screen에 보여주게 될 것입니다.
 
Test3은 실질적인 data를 테스트합니다. 위에서 작성한 MockData를 returnValue에 넣어서 data를 모킹할 수 있습니다. 예제에서는 10개의 MockData를 주입해주었고, testid를 통해 총 몇개가 렌더링 되어있는지를 체크하였습니다.
notion image
모든 테스트가 성공적으로 통과한 것을 확인할 수 있습니다. 사실 함수를 mocking 하는 방식은 여러가지가 있습니다. mockReturnValue 말고 mockReturnValueOnce 를 사용할 수도 있습니다.
아직 모든 mock 함수를 알지 못해서 하나하나 공부해가며 블로그에 포스팅 하도록 하겠습니다.
위 예제 코드는 깃허브 에 올려두었습니다! 참고할만한 내용까진 아닐 것 같지만 그래도 혹시 궁금하시다면 한번씩 보고 피드백 남겨주세요!