React Router v6 の自動テスト

# はじめに
この記事では、[**_React Router_**](https://reactrouter.com/en/main) を利用したWebアプリの自動テスト実装方法について記載していきます。

# 前提
開発環境は以下の通りです。

- Windows11
- VSCode
- TypeScript 4.9.5
- React 18.2.0
- React Router 6.10.0
- Vite 4.1.0
- Vitest 0.28.5
- @testing-library/react 14.0.0
- jsdom 21.1.0

また、事前に以下の準備をしています。
1. Vite でプロジェクトファイルを作成
1. VitestでReact Testing Libraryを使えるように設定

※詳細はそれぞれ別の記事にまとめています。

https://qiita.com/Yasushi-Mo/items/a85bb7a90ce96deae879

https://qiita.com/Yasushi-Mo/items/0a29df164b02bd7d78e6

# デフォルトルートのテスト
[`Route`](https://qiita.com/Yasushi-Mo/items/0a29df164b02bd7d78e6) を利用したアプリ起動時のデフォルトのルートのテストをします。

## アプリの実装
まず、`App` コンポーネントを [`BrowserRouter`](https://reactrouter.com/en/main/router-components/browser-router) で囲み、ルーティングができるようにします。
```main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

```
次にルート（`"/"`）アクセス時に `Home` コンポーネントを開くように実装します。
```src/pages/Home.tsx
export const Home = () => {
  return <div>You are home</div>;
};
```

```src/App.tsx
import { Route, Routes } from "react-router-dom";
import { Home } from "./pages/Home";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  );
}

export default App;
```
実装できたので、動作確認をします。ローカルサーバーを立ち上げ、ブラウザで http://localhost:3000/ にアクセスすると、Homeページが表示されることを確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/05e0443f-2a2d-3307-f540-d0f627db17ff.png)

## テストの実装
`App` コンポーネントをレンダリングし、 「you are home」と表示されることをテストします（アプリ起動時に `Home` コンポーネントが開くことをテストします）。

```src/__tests__/integration/App.test.tsx
import { render, screen } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { describe, test } from "vitest";
import App from "../../App";

describe("App routing", () => {
  test("default root", () => {
    render(<App />, { wrapper: BrowserRouter });

    // verify page content for default route
    expect(screen.getByText(/you are home/i)).toBeInTheDocument();
  });
});
```
テストを通すことができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/6dd85b3a-69ec-999d-dfc6-7ed2cdb012ae.png)

`render` の第2引数の [`wrapper`](https://testing-library.com/docs/react-testing-library/api/#wrapper) は、第1引数のコンポーネント（コンテナ）を `BrowserRouter` でラップするために追加しています。アプリ側のコードでは、`App` を `BrowserRouter` でラップしているので、テストコードでも同様にラップする必要があります。ラップしないと、エラーが発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/d8f706f1-91ff-714c-7232-e3d0ccf285cd.png)
なお、第2引数ではなく、第1引数でラップすることも可能です。
```src/__tests__/integration/App.test.tsx
render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
```

# リンクのテスト
[`Link`](https://reactrouter.com/en/main/components/link) を利用したページ上のリンクをクリックした際のページ遷移のテストをします。

## アプリの実装
まず、遷移先のページである `About` コンポーネントを作成し、ルーティングに追加します。
```src/pages/Home.tsx
export const About = () => {
  return <div>You are on the about page</div>;
};
```
```src/App.tsx
import { Route, Routes } from "react-router-dom";
import { About } from "./pages/About";
import { Home } from "./pages/Home";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
    </Routes>
  );
}

export default App;
```
`Home` ページ上で、リンクをクリックしたら、`About` ページに遷移するようにします。
```src/pages/Home.tsx
import { Link } from "react-router-dom";

export const Home = () => {
  return (
    <div>
      <div>You are home</div>
      <Link to="/about">About</Link>
    </div>
  );
};
```
動作確認をします。
![ezgif-1-2342bed04c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/7287476f-8a94-ba3f-b72b-b020092b375d.gif)

## テストの実装
`App` コンポーネントをレンダリングした後、リンクをクリックし、その後、ページ上に「you are on the about page」と表示されることをテストします。
```src/__tests__/integration/App.test.tsx
test("link", async () => {
  render(<App />, { wrapper: BrowserRouter });
  const user = userEvent.setup();

  // verify page content for expected route after navigating
  await user.click(screen.getByRole("link"));
  expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument();
});
```
なお、ページ上のリンクが1つだけの場合、上記の書き方でも問題ありませんが、以下のように複数ある場合、エラーになります。
```src/pages/Home.tsx
import { Link } from "react-router-dom";

export const Home = () => {
  return (
    <div>
      <div>You are home</div>
      <Link to="/about">About</Link>
      <br />
      <Link to="/form">Form</Link>
    </div>
  );
};
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/f06863e7-9d93-fffb-3a42-f23bcbcf0cd9.png)

解決方法は複数ありますが、例えば、第2引数に [`name`](https://testing-library.com/docs/queries/byrole/) を追加することで、クリック対象のリンク（`About`）を特定させることができます。
```src/__tests__/integration/App.test.tsx
test("navigate from root to about by clicking link", async () => {
  render(<App />, { wrapper: BrowserRouter });
  const user = userEvent.setup();

  // verify page content for expected route after navigating
  await user.click(screen.getByRole("link"), { name: "About" });
  expect(screen.getByText(/you are on the about page/i)).toBeInTheDocument();
});
```

# リダイレクトのテスト
[`Navigate`](https://reactrouter.com/en/main/components/navigate) を利用したリダイレクトのテストをします。

## アプリの実装
ルート（`"/"`）アクセス時に `"/home"` へリダイレクトするようにします。
```src/App.tsx
import { Navigate, Route, Routes } from "react-router-dom";
import { About } from "./pages/About";
import { Home } from "./pages/Home";

function App() {
  return (
    <Routes>
      <Route path="/" element={<Navigate to="/home" />} />
      <Route path="/home" element={<Home />} />
      <Route path="/about" element={<About />} />
    </Routes>
  );
}

export default App;
```
動作確認をします。
![ezgif-1-c311ad5c50.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/babf16e0-d941-fb77-41b5-211bc0d7a2e7.gif)

## テストの実装
`App` コンポーネントをレンダリングした後、パスが `"/home"` へ変わっていることを確認します。
まず、テストでパスを確認するため、パス確認用のコンポーネントを作成します。
```src/__tests__/libs/LocationDisplay.tsx
import { useLocation } from "react-router-dom";

export const LOCATION_DISPLAY = "location-display";

export const LocationDisplay = () => {
  const location = useLocation();

  return <div data-testid={LOCATION_DISPLAY}>{location.pathname}</div>;
};
```
次にパス確認用のコンポーネントをテストコード上にレンダリングし、そのコンポーネントのパスが `"/home"` であることをテストします。
```src/__tests__/integration/App.test.tsx
test("default root", () => {
  render(
    <>
      <App />
      <LocationDisplay />
    </>,
    { wrapper: BrowserRouter }
  );

  // verify page content for default route
  expect(screen.getByText(/you are home/i)).toBeInTheDocument();

  // verify page path redirected from root to home
  expect(screen.getByTestId(LOCATION_DISPLAY)).toHaveTextContent("/home");
});
```

# 最後に
今回は React Router の [`Route`](https://qiita.com/Yasushi-Mo/items/0a29df164b02bd7d78e6)、[`Link`](https://reactrouter.com/en/main/components/link)、[`Navigate`](https://reactrouter.com/en/main/components/navigate) を利用したWebアプリの自動テストを実装しました。React Router にはまだまだたくさんの機能があるので、今後も自動テストを実装できるか確認していければと思っています。

# 参考
- [API | Testing Library](https://testing-library.com/docs/react-testing-library/api/#wrapper)
- [BrowserRouter v6.10.0 | React Router](https://reactrouter.com/en/main/router-components/browser-router)
- [ByRole | Testing Library](https://testing-library.com/docs/queries/byrole/)
- [Navigater v6.10.0 | React Router](https://reactrouter.com/en/main/components/navigate)
- [React Router | Testing Library](https://testing-library.com/docs/example-react-router/)
- [Router v6.10.0 | React Router](https://qiita.com/Yasushi-Mo/items/0a29df164b02bd7d78e6)