1편에 이어 조금 난이도를 올려 테스트 코드를 작성해보겠습니다.
라우팅이 있는 컴포넌트를 테스트 해보겠습니다.
코드를 작성하기에 앞서 사전지식 2개를 알고 가는게 좋을 거 같습니다. (스냅샷, MemoryRouter)
스냅샷: 컴포넌트의 랜더링 결과물을 일종의 스냅샷으로 만들어놓고, 이전에 만들어놓은 스냅샷과 비교하여 같은지 다른지를 확인하는 테스트 방법입니다. 이를 통해 컴포넌트의 디자인이나 레이아웃 변경 등에 의해 예기치 않게 결과물이 변경되는 것을 빠르게 감지할 수 있습니다.
snapshots 디렉터리에 자동적으로 생성되고 저장되는 방식입니다. 정적인 컴포넌트에 활용을 하면 좋습니다.
MemoryRouter: 우리가 보통 react-router-dom을 이용해서 라우팅을 할때 최상위에
BrowserRouter 감싸고 해야 라우팅이 가능합니다. 하지만 테스트코드에서 라우팅을 할때는MemoryRouter를 사용을 하면 브라우저의 URL주소를 변경하지 않고 메모리상에서 URL을 관리하기 때문에 테스트를 실행하는 환경에서 브라우저가 없거나 브라우저 동작이 필요하지 않는경우에 적합합니다.
일단 테스트 해볼 컴포넌트는 검색이 되는 입력창(Search Header) 해볼 예정입니다.
/:keyword 파라미터가 url에 입력이 되면 헤더 컴포넌트에 있는 입력창에 value로 입력이 되고 버튼을 클릭하면 /products/:sekeywordarch 이렇게 라우팅이 되는 컴포넌트입니다.
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
export default function SearchHeader() {
const { keyword } = useParams();
const [search, setSearch] = useState("");
const navigate = useNavigate();
const handleInputChange = (event) => {
setSearch(event.target.value);
};
useEffect(() => {
setSearch(keyword || "");
}, [keyword]);
const handleSearch = () => {
navigate(`/products/${search}`);
};
return (
<div>
<input type="text" value={search} onChange={handleInputChange} />
<button onClick={handleSearch}>Search</button>
</div>
);
}
import renderer from "react-test-renderer";
import SearchHeader from "../SearchHeader";
import { withMemoryRouter } from "./utils";
import { Route } from "react-router-dom";
import { fireEvent, render, screen } from "@testing-library/react";
describe("Search Header", () => {
it("renders snapshot", () => {
const searchHeader = renderer
.create(withMemoryRouter(<Route path="/" element={<SearchHeader />} />))
.toJSON();
expect(searchHeader).toMatchSnapshot();
});
it("동적 url로 접속했을때 입력창에 keyword가 들어가는지 확인", () => {
// given
render(
withMemoryRouter(
<Route path="/:keyword" element={<SearchHeader />} />,
"/shoes"
)
);
expect(screen.getByDisplayValue("shoes")).toBeInTheDocument();
});
it("입력창에 값을 입력하고 인풋 버튼을 클릭했을때 products 페이지로 이동", () => {
const fakeSearch = "shoes";
render(
withMemoryRouter(
<>
<Route path="/" element={<SearchHeader />} />,
<Route
path={`/products/:keyword`}
element={<span data-testid="search-result" />}
/>
</>
)
);
const searchBtn = screen.getByRole("button");
const searchInput = screen.getByRole("textbox");
fireEvent.change(searchInput, { target: { value: fakeSearch } });
fireEvent.click(searchBtn);
expect(screen.getByTestId("search-result")).toBeInTheDocument();
});
});
// withMemoryRouter.js
import { MemoryRouter, Routes } from "react-router-dom";
export function withMemoryRouter(routes, initialEntry = "/") {
return (
<MemoryRouter initialEntries={[initialEntry]}>
<Routes>{routes}</Routes>
</MemoryRouter>
);
}
첫번째는 snapshot 테스팅이고 실행했을때 test 폴더안에 snapshot 폴더가 생성이 되고 .snap 파일이 정상적으로 만들어졌는지 확인을 하면 됩니다.
두번째는 getByDisplayValue를 사용했는데 input, textarea 등과 같은 엘리먼트에서 사용자 입력 값을 보여주는 속성이고 value 값이 일치하는 엘리먼트를 가져오는 함수입니다.
그래서 동적 url로 접속했을때 shoes가 정상적으로 input element value에 들어갔는지 확인.
세번째 Routing 테스트 뿐만 아니라 테스트를 할때는 내가 지금 테스트할 컴포넌트가 아닌 다른 실제컴포넌트를 사용을 하면 의도치 않은 부수효과가 발생할 수 있습니다. 예를 들어, 컴포넌트가 네트워크 요청이나 상태 변화를 처리한다면 테스트중에 부수효과가 발생할 경우 테스트 결과를 신뢰 할 수 없습니다. 결론은 내가 지금 테스트할 컴포넌트에 대해서만 테스트를 하면 됩니다.
fireEvent.change를 이용해서 input 엘리먼트에 value를 강제로 입력을 하고 버튼을 클릭 했을때 라우팅이 되는 로직입니다. 여기서는 더미 컴포넌트를 사용해서 정상적으로 라우팅이 되는지만 확인 하는 테스트를 했습니다.
자주 반복되는 컴포넌트는 공통적으로 묶어서 사용하면 코드가 깔끔하게 작성이 되고 유지보수에 용이합니다.
테스트를 실행을 해보고 커버리지도 100%가 되는지 확인하면 됩니다.
계속 테스트 코드를 작성해볼 예정