Next.js - pathname에 따라 변경되는 UI와 next/link 를 테스트해보자

방구석 코딩쟁이·2024년 1월 29일
2

pathname에 따라 변경되는 UI를 테스트

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를 판단할 때는 테스트가 잘 되지 않았습니다
물론, 제가 좀 더 살펴보면 가능할지도 모르겠으나 공식문서의 예제로는 제가 원하는대로 테스트가 되지 않았습니다.

  • 공식문서의 예시로는 특정 URL로 이동하는 버튼을 클릭했을 때 잘 이동하는지에 대해서 테스트를 하고 있었습니다.

이와 같은 상황을 테스트해보고 싶으신 분을 위해 모킹한 소스코드를 아래에 첨부해두겠습니다.

// 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 함수를 모킹하는 방법이었습니다.

jestmock함수를 이용하여 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("/");
  });
});

이렇게 테스트 코드를 작성하면 다음과 같이 잘 통과하는 것을 볼 수 있습니다.

profile
풀스택으로 나아가기

0개의 댓글