1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【react-router + Jest】useNavigateをJestでモック化できない

Posted at

はじめに

useNavigateを使用しているコンポネーントのテストを行う際に、useNavigateのモック化が理解できず、テスト実行時にエラーとなりました。useNavigateのモック化方法をまとめてみました。

問題

useNavigateのモック化してテスト実行すると下記のエラーが発生しました。

● UserCard › 戻るボタンをクリックすると/に遷移すること
    expect(jest.fn()).toHaveBeenCalledWith(...expected)
    Expected: "/"
    Received
           1: called with 0 arguments
           2: called with 0 arguments
    Number of calls: 2
      84 |     await userEvent.click(removeButton);
      85 |
    > 86 |     expect(useNavigate).toHaveBeenCalledWith("/");
         |                         ^
      87 |   });
      88 | });
      89 |
      at Object.<anonymous> (src/tests/userCardComponent.spec.tsx:86:25)
 PASS  src/tests/sampleComponent.spec.tsx

Test Suites: 1 failed, 1 passed, 2 total                                                                                                                                                                                  
Tests:       1 failed, 7 passed, 8 total
Snapshots:   0 total
Time:        1.923 s

原因

useNavigate自体をモック関数にしてしまっていました。

React Router の useNavigate は 関数を返す関数

useNavigateは関数を返す関数であることを理解できていませんでした。

テスト対象のコンポーネントでは確かに返却された関数をnavigateに格納して使用していました。

const navigate = useNavigate(); // useNavigateで返却された関数をnavigateに格納
export const UserCard = () => {
  return (
    <Button
      w="100%"
      mt={4}
      colorScheme="teal"
      data-testid="remove-button"
      onClick={() => navigate("/")}  // navigateで実行!
    >
      戻る
    </Button>
  );
};

実際に書いていたテスト

jest.mock("react-router", () => ({
  ...jest.requireActual("react-router"),
  useNavigate: jest.fn(), // これだとuseNavigate()がモック関数になり、使用時はundefinedを返す
}));

解決方法

モック関数(mockNavigate)を別途作り、useNavigateが呼ばれた際はモック関数を呼ぶようにしました。

+const mockNavigate = jest.fn();
jest.mock("react-router", () => ({
  ...jest.requireActual("react-router"),
+  useNavigate: () => mockNavigate,
-   useNavigate: jest.fn(),
}));
  1. mockNavigate というモック関数を作成
  2. ...jest.requireActual("react-router"),jest.requireActual("react-router")でreact-routerの全てのモジュールをエクスポート、...(スプレッド構文)でコピー
  3. useNavigate が呼ばれたときに mockNavigate を返すようにする
  4. コンポーネントでは const navigate = useNavigate() と書かれており、navigate には mockNavigate が入る
  5. navigate("/") が呼ばれると、実際には mockNavigate("/") が実行される

完成系

テストコード
test.tsx
jest.mock("../utils/supabaseFunctions", () => ({
  getUserById: jest.fn(),
}));

const mockNavigate = jest.fn();
jest.mock("react-router", () => ({
  ...jest.requireActual("react-router"),
  useNavigate: () => mockNavigate,
}));

// モックデータを作成
const mockUser = {
  id: "apple",
  name: "テストユーザー",
  description: "<p>テストユーザーの説明</p>",
  skill_name: "React",
  github_id: "testuser",
  qiita_id: "testuser",
  x_id: "testuser",
  getGithubUrl: jest.fn(() => "https://github.com/testuser"),
  getQiitaUrl: jest.fn(() => "https://qiita.com/testuser"),
  getXUrl: jest.fn(() => "https://x.com/testuser"),
};

beforeEach(() => {
  jest.clearAllMocks();
  // DBから取得したデータをモックする
  (getUserById as jest.Mock).mockResolvedValue([mockUser]);
});

describe("UserCard", () => {
  it("戻るボタンをクリックすると/に遷移すること", async () => {
    render(<UserCard />, { wrapper: BrowserRouter });

    // 戻るボタンをクリック
    const removeButton = await screen.findByTestId("remove-button");
    await userEvent.click(removeButton);

    expect(mockNavigate).toHaveBeenCalledWith("/");
  });
});
テスト対象のコンポーネント
UserCard.tsx
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { getUserById } from "../utils/supabaseFunctions";
import { User } from "../domain/user";
import { Box, Button, Card, CardBody, Heading } from "@chakra-ui/react";
import { IoLogoGithub } from "react-icons/io5";
import { SiQiita } from "react-icons/si";
import { RiTwitterXFill } from "react-icons/ri";

export const UserCard = () => {
  const { id } = useParams();
  const [loadingFlag, setLoadingFlag] = useState(true);
  const [user, setUser] = useState<User[]>();
  const navigate = useNavigate();
  const getData = async () => {
    const data = await getUserById(id!);
    setUser(data);
    if (data.length !== 0) {
      setLoadingFlag(false);
    }
  };
  useEffect(() => {
    getData();
  }, []);
  if (loadingFlag) {
    return <h1>loading…</h1>;
  }
  return (
    <Box
      height="100vh"
      display="flex"
      flexDirection="column"
      alignItems="center"
      justifyContent="center"
      bg="#C4F1F9"
    >
      <Box width="sm" p={4}>
        {user?.map((user) => {
          return (
            <Card key={user.id}>
              <CardBody>
                <Box>
                  <Heading as="h1" size="lg" mb={4} data-testid="name">
                    {user.name}
                  </Heading>
                </Box>
                <Box mb={2}>
                  <Heading as="h2" size="md">
                    好きな技術
                  </Heading>
                  <div data-testid="favorite_skill">{user.skill_name}</div>
                </Box>
                <Box display="flex" justifyContent="center">
                  <Box w="33%" textAlign="center">
                    <a href={user.getGithubUrl(user.github_id)}>
                      <IoLogoGithub data-testid="github-icon" fontSize="40px" />
                    </a>
                  </Box>
                  <Box w="33%" textAlign="center">
                    <a href={user.getQiitaUrl(user.qiita_id)}>
                      <SiQiita data-testid="qiita-icon" fontSize="40px" />
                    </a>
                  </Box>
                  <Box w="33%" textAlign="center">
                    <a href={user.getXUrl(user.x_id)}>
                      <RiTwitterXFill data-testid="x-icon" fontSize="40px" />
                    </a>
                  </Box>
                </Box>
              </CardBody>
            </Card>
          );
        })}
      </Box>
      <Box width="sm" p={4} textAlign="center">
        <Button
          w="100%"
          mt={4}
          colorScheme="teal"
          data-testid="remove-button"
          onClick={() => navigate("/")}
        >
          戻る
        </Button>
      </Box>
    </Box>
  );
};

おわりに

関数が何を返すのかも意識して、テストしないといけないと思いました。
今回エラーに向き合えたことで、他のHooksでモック化する際にも応用ができそうです。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?