next.js와 테스트 코드를 공부하던 도중에 pathname에 따라 달라지는 UI를 테스트하는 방법에 대해 찾아보게 되었습니다.
제가 테스트하고 싶은 컴포넌트는 아래와 같습니다.
"use client";
import { usePathname } from "next/navigation";
import React from "react";
import styled from "styled-components";
const Container = styled.div``;
const Title = styled.h1``;
export default function PageHeader() {
const pathname = usePathname();
let title = "";
if (pathname === "/add") title = "할 일 추가";
else if (pathname === "/") title = "할 일 목록";
else title = "Not Found";
return (
<Container>
<Title>{title}</Title>
</Container>
);
}
제가 찾아본 자료는 Github Issue이며 링크는 아래에 첨부해두겠습니다.
첫 번째로 찾아본 자료는 next-router-mock
라이브러리를 활용하여 테스트를 하는 방법이었습니다. 다만 이 방법은 제가 생각했던 대로 동작하지 않았는데, 그 이유는 이는 URL을 기반으로 테스트를 하는 방법이었기 때문입니다. router를 mocking하여 해당 URL로 이동했을 경우 URL이 맞는지, 아닌지 여부를 테스트할 때는 유용했으나 URL에 따른 UI를 판단할 때는 테스트가 잘 되지 않았습니다
물론, 제가 좀 더 살펴보면 가능할지도 모르겠으나 공식문서의 예제로는 제가 원하는대로 테스트가 되지 않았습니다.
이와 같은 상황을 테스트해보고 싶으신 분을 위해 모킹한 소스코드를 아래에 첨부해두겠습니다.
// https://github.com/vercel/next.js/discussions/42527
import mockRouter from "next-router-mock";
import { createDynamicRouteParser } from "next-router-mock/dynamic-routes";
jest.mock("next/router", () => jest.requireActual("next-router-mock"));
mockRouter.useParser(
createDynamicRouteParser([
// @see https://github.com/scottrippey/next-router-mock#dynamic-routes
])
);
jest.mock<typeof import("next/navigation")>("next/navigation", () => {
const actual = jest.requireActual("next/navigation");
const nextRouterMock = jest.requireActual("next-router-mock");
const { useRouter } = nextRouterMock;
const usePathname = jest.fn().mockImplementation(() => {
const router = useRouter();
return router.asPath;
});
const useSearchParams = jest.fn().mockImplementation(() => {
const router = useRouter();
return new URLSearchParams(router.query);
});
console.log("useSearchParams", useSearchParams);
console.log("useRouter", useRouter);
return {
...actual,
useRouter: jest.fn().mockImplementation(useRouter),
usePathname,
useSearchParams,
};
});
export { mockRouter };
그래서 또 다른 방법을 찾으러 나섰습니다. 두번째 방법은 usePathname
함수를 모킹하는 방법이었습니다.
jest
의 mock
함수를 이용하여 usePathname
함수을 mocking하고 mockImplementation
함수를 이용하여 상황에 맞게 구현하는 코드를 통해서 UI 테스트를 할 수 있게 되었습니다.
Github Issue에서 예시로 든 코드는 아래와 같습니다.
const mockUsePathname = jest.fn();
jest.mock('next/navigation', () => ({
usePathname() {
return mockUsePathname();
},
}));
test('with the pathname of "/home"', () => {
mockUsePathname.mockImplementation(() => '/user');
// ...
});
test('with the pathname of "/blogs/100"', () => {
mockUsePathname.mockImplementation(() => '/blogs/100');
// ...
});
이 코드를 보며 제 코드에 적용시킨 결과는 아래와 같으며 테스트 결과도 잘 나왔음을 확인할 수 있었습니다.
// https://github.com/vercel/next.js/discussions/42527
import PageHeader from "@/components/PageHeader";
import { render, screen } from "@testing-library/react";
// https://github.com/amannn/next-intl/discussions/331
const mockUsePathname = jest.fn();
jest.mock("next/navigation", () => ({
usePathname() {
return mockUsePathname();
},
}));
describe("<PageHeader/>", () => {
it("renders component correctly", () => {
mockUsePathname.mockImplementation(() => "/");
render(<PageHeader />);
const label = screen.getByText("할 일 목록");
expect(label).toBeInTheDocument();
});
it('renders component correctly when pathname is "/add"', () => {
mockUsePathname.mockImplementation(() => "/add");
render(<PageHeader />);
const label = screen.getByText("할 일 추가");
expect(label).toBeInTheDocument();
});
});
테스트 코드를 어떻게 짜야 좋을지는 계속해서 공부해봐야 겠지만 일단 내가 생각한대로 테스트코드를 작성할 수 있는 능력을 기르는 것도 중요한 것 같습니다...
<Link/>
를 눌렀을 때, 해당 경로로 잘 이동하는지를 테스트해보기 위해서는 이전에 봤었던 next-router-mock
패키지를 활용해야 되었습니다.
<Link>
를 누른 경우, 해당 경로로 이동하는지를 테스트를 해보고 싶었습니다.
테스트해보고 싶은 컴포넌트는 다음과 같습니다.
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
import styled from "styled-components";
const Container = styled.div``;
const Title = styled.h1``;
const GoBack = styled(Link)``;
export default function PageHeader() {
const pathname = usePathname();
let title = "";
if (pathname === "/add") title = "할 일 추가";
else if (pathname === "/") title = "할 일 목록";
else if (pathname.startsWith("/detail")) title = "할 일 상세";
else title = "에러😕";
return (
<Container>
<Title>{title}</Title>
{pathname !== "/" && <GoBack href="/">돌아가기</GoBack>}
</Container>
);
}
저희는 /error
일 때, <GoBack />
컴포넌트가 있는지를 테스트해보려고 하고, <GoBack />
컴포넌트의 href
속성이 /
인지 테스트해보고 싶습니다.
그 다음에는 <GoBack />
을 누른 경우, path
가 /
로 가는지를 테스트 해봐야 합니다.
그럼 첫 번째 테스트를 작성하도록 합시다.
it("renders component correctly with Error", () => {
mockUsePathname.mockImplementation(() => "/error");
render(<PageHeader />);
const label = screen.getByText("에러😕");
expect(label).toBeInTheDocument();
const goBack = screen.queryByText("돌아가기");
expect(goBack).toBeInTheDocument();
expect(goBack).toHaveAttribute("href", "/");
});
이전에 작성했던 테스트와 유사합니다.
그 다음에는 클릭이벤트가 발생했을 때, 해당 링크로 잘 이동시키는지를 확인하도록 합시다.
import { mockRouter } from "./next-router-utils";
import { MemoryRouterProvider } from "next-router-mock/MemoryRouterProvider";
...
// 중간 생략
...
it("renders component correctly with goBack Link", async () => {
mockUsePathname.mockImplementation(() => "/error");
render(<PageHeader />, { wrapper: MemoryRouterProvider });
const goBack = screen.getByRole("link");
fireEvent.click(goBack);
// https://www.npmjs.com/package/next-router-mock#example-nextlink-with-react-testing-library
// 이동한 경로가 "/"인지 확인
await waitFor(() => {
expect(mockRouter.pathname).toEqual("/");
});
});
이렇게 테스트 코드를 작성하면 다음과 같이 잘 통과하는 것을 볼 수 있습니다.