2022. 1. 11. 00:45ㆍReactJS/Information
들어가기 전
이 글은 GraphQL에 대해서 테스트를 작성하는 것이 아니다.
React-Query에 대해서 작성하는 것이다.
모든 코드들은 Typescript로 작성되었으며, 선행 지식이 필요하다.
선행 자료 참고 문서)
[React.js] React + Typescript 테스트 코드 작성하기
[React.js] React-Testing-Library 사용하기
[React.js] React Query(useQuery, useMutation)
완성된 코드)
Typescript + React-Query를 사용해 테스트 코드를 작성하는 것은 자료를 찾아도 너무 안나온다.
많은 키워드로 검색해도 내가 원하는 답변을 찾기가 어려워 테스트 코드를 작성하는 것이 힘들었다.
API 호출을 위해 axios를 사용하였으며 CRA를 이용해 코드를 진행시켰다.
사용 API 문서
https://jsonplaceholder.typicode.com/ 에서 제공하고 있는 API를 이용해 글을 받아오도록 한다.
코드 리뷰
/src/errors/api/ApiCommunicationError.ts
export const ApiCommunicationError = (): Error => {
throw new Error("API 통신중 오류가 발생했습니다.")
}
/src/service/axios/axios.ts
import axios, {AxiosInstance, AxiosRequestConfig, AxiosResponse} from "axios";
import {ApiCommunicationError} from "../../errors/api/ApiCommunicationError";
axios.defaults.baseURL = "https://jsonplaceholder.typicode.com";
axios.defaults.headers.common['Content-Type'] = 'application/json';
export interface CustomInstance extends AxiosInstance {
get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>;
}
export const customAxios: CustomInstance = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
});
customAxios.interceptors.response.use((response: AxiosResponse) => {
if (response.status !== 200) {
throw ApiCommunicationError()
}
return response.data;
})
axios를 편하게 사용하기 위해 위와 같이 커스터마이징 처리했다.
axios의 Return Type을 Generic하게 설정하기 위함이다.
추가로 Interceptor를 구현해 Generic Type을 바로 받을 수 있도록 처리하였다.
이제 React-Query를 이용한 API 서비스를 구현할 것인데, React-Query를 테스트하기 위해서는 Custom Hook 형식으로 만들어야 한다.(React-Query 공식문서 참조)
/src/model/post/IPost.ts
export interface IPost {
userId: number,
id: number,
title: string,
body: string,
}
/src/service/api/apiService.ts
import {customAxios} from "../axios/axios";
import {IPost} from "../../model/post/IPost";
const throwError = (message: string | any) => {
throw new Error(message)
}
export const fetchPost = async (id: number): Promise<IPost> => {
try {
return await customAxios.get<IPost>(`/posts/${id}`)
} catch (e) {
return throwError(e)
}
}
위에 작성한 API 제공 URL에서 posts라는 End-point를 통해 특정 게시물을 get하는 것이다.
이에 대한 응답 값으로는 IPost.ts 와 동일하다.
이제 위 함수를 사용하는 Custom Hook을 작성하면 된다.
작성하기 전 useQuery에 대한 응답값을 Type으로 만들어줘야 한다.
나중에 테스트 코드에서도 사용할것이고, 이를 사용하면 Type을 정확하게 알 수 있기 때문이다.
(Typescript를 쓰는 이유중 하나가 Type에 대해 확실하게 보장받기 위해서인데 이를 적극 활용하자.)
/src/types/hooks/QueryReturnType.ts
import {QueryObserverResult, RefetchOptions, RefetchQueryFilters} from "react-query/types/core/types";
export type QueryReturnType<R, E> = {
data: R | undefined,
error: E | null,
isLoading: boolean,
isError: boolean,
againExecute?: <TPageData>(options?: RefetchOptions & RefetchQueryFilters<TPageData>) => Promise<QueryObserverResult<R, E>>
}
React-Query에서 useQuery Hook에 대한 Return Type을 거의 그대로 따온것이다.
이 역시 Generic Type을 받아 사용할 수 있게 해주면 확장성에 큰 도움이 된다.
(API 응답 타입에 따라 Type을 계속 선언하면 그게 더 일이다.)
/src/services/api/customHooks/useFetchPost.ts
import {fetchPost} from "../apiService";
import {useQuery} from "react-query";
import {QueryReturnType} from "../../../types/hooks/QueryReturnType";
import {IPost} from "../../../model/post/IPost";
export const useFetchPost = (id: number): QueryReturnType<IPost, Error> => {
const {
data,
error,
isLoading,
isError,
refetch
} = useQuery<IPost, Error>(["GET_POST", id], async () => fetchPost(id), {
refetchOnWindowFocus: false,
keepPreviousData: true,
retry: false,
enabled: false,
cacheTime: Infinity
}
)
return {
data: data,
error: error,
isLoading: isLoading,
isError: isError,
againExecute: refetch
}
}
useQuery를 사용해 아까 만들었던 apiService.ts의 fetchPost 함수를 작동시키는 것이다.
위와 같이 Custom Hook을 만들어주고, 테스트 코드를 작성해야 한다.
본격적인 테스트 코드 작성 전 MockImplementation을 이용해 함수를 가짜로 구현할 것인데 이를 매번 해주면 반복적인 코드가 너무 많이 들어가게 되니 이 역시 따로 빼주도록 하자.
/src/__test__/MockImplementation.ts
import {QueryReturnType} from "../types/hooks/QueryReturnType";
type ReturnType<R, E> = () => QueryReturnType<R, E>
interface IFunction<R, E> {
data: R | undefined, error: E | null, isError: boolean, isLoading: boolean
}
export function setUseQueryMockImplementation<R, E>({data, error, isError, isLoading}: IFunction<R, E>): ReturnType<R, E> {
return () => {
return {
data: data,
error: error,
isError: isError,
isLoading: isLoading,
}
}
}
위 함수로 useQuery에 대한 테스트 코드를 작성할 때 MockImplementation을 조금 더 간편하게 할 수 있다.
그리고 테스트중 사용할 가짜 테스트 데이터 또한 따로 빼주도록 하자.
/src/__test__/__fixture__/MockPostData.ts
import {IPost} from "../../model/post/IPost";
export const MockPostData: IPost = {
id: 1,
userId: 1,
title: "y0ngha react-tdd",
body: "jest!"
}
이제 준비는 끝났고, App에 테스트 코드를 작성하면 된다.
/src/App.test.tsx
import React from 'react';
import App from "./App";
import {render} from "@testing-library/react";
import {useFetchPost} from "./services/api/customHooks/useFetchPost";
import {IPost} from "./model/post/IPost";
import {setUseQueryMockImplementation} from "./__test__/MockImplementation";
import {MockPostData} from "./__test__/__fixture__/MockPostData";
jest.mock("./services/api/customHooks/useFetchPost")
const mockUseFetchPost = useFetchPost as jest.MockedFunction<typeof useFetchPost>;
describe("renders app", () => {
beforeEach(() => {
mockUseFetchPost.mockImplementation(setUseQueryMockImplementation<IPost, Error>({
data: undefined,
error: null,
isError: false,
isLoading: false
}))
});
afterEach(() => {
jest.clearAllMocks();
});
it("Loading Test", () => {
mockUseFetchPost.mockImplementation(setUseQueryMockImplementation<IPost, Error>({
data: undefined,
error: null,
isError: false,
isLoading: true
}))
const wrapper = render(<App/>)
expect(mockUseFetchPost).toBeCalledTimes(1)
expect(mockUseFetchPost).toBeCalledWith(1)
expect(wrapper.getByRole("isLoadingSpan").textContent).toContain("로딩중입니다..")
})
it("Error Test", () => {
mockUseFetchPost.mockImplementation(setUseQueryMockImplementation<IPost, Error>({
data: undefined,
error: new Error("알 수 없는 오류 발생"),
isError: true,
isLoading: false
}))
const wrapper = render(<App/>)
expect(mockUseFetchPost).toBeCalledTimes(1)
expect(mockUseFetchPost).toBeCalledWith(1)
expect(wrapper.getByRole("isErrorSpan").textContent).toContain("알 수 없는 오류 발생")
})
it("Result Fetch Test", () => {
mockUseFetchPost.mockImplementation(setUseQueryMockImplementation<IPost, Error>({
data: MockPostData,
error: null,
isError: false,
isLoading: false
}))
const wrapper = render(<App/>)
expect(mockUseFetchPost).toBeCalledTimes(1)
expect(mockUseFetchPost).toBeCalledWith(1)
const data = mockUseFetchPost(1)
expect(data.data).toEqual(MockPostData)
expect(wrapper.getByRole("isResultSpan").textContent).toContain("y0ngha react-tdd")
})
})
useFetchPost 함수에 대해 Mocking 해주고 이를 가짜로 구현하면 된다.
API를 테스트할 때에는 "성공했는가"에 초점을 두지 말고 "성공했을 때", "오류가 발생했을 때", "로딩중일 때" 를 테스트하면 조금 더 명확한 테스트 진행이 가능하다.
사실 기존에 Jest와 React-Query를 다루어 봤다면 위 코드를 해석하는데에는 크게 문제가 없을 것이다.
이 코드에서 중요한 포인트는 테스트코드보다는 MockImplementation, Type이 중요하다.
위 방법을 쉽게, 그리고 동작하는 코드를 찾는데 시간이 많이 걸렸다.
'ReactJS > Information' 카테고리의 다른 글
[React.js] React 18 뭐가 달라질까? (0) | 2022.02.04 |
---|---|
[React.js] React에서의 Hydrate (0) | 2022.02.03 |
[React.js] React + Typescript 테스트 코드 작성하기 (0) | 2022.01.04 |
[React.js] React-Testing-Library 사용하기 (0) | 2022.01.03 |
[React.js] GraphQL 서버와 통신하기 (0) | 2021.12.21 |