入れ子の目次で分かる Container / Presentational パターンのテスト容易性

お疲れ様です。eヘルスケアの袋瀬です。

この記事では react で「Container / Presentational パターンはテストがしやすい」という事を
入れ子の目次を作成する過程で見ていきたいと思います。

## 入れ子の目次はデータの整形が大変だった

最近、業務で以下のような目次を作成する必要がありました。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1265134/cb0d2941-238a-9f74-6c48-3b4c24e1a572.png"></img>

この目次は、headlessCMS(microCMS)からデータ連携され、そのデータ構造は以下となります。(目次に必要なデータだけを抜粋)

```js
headers: [
  {
    fieldId: "mainHeader",
    text: "大見出し1",
  },
  {
    fieldId: "subHeader",
    text: "小見出し1-1",
  },
  {
    fieldId: "mainHeader",
    text: "大見出し2",
  },
  // 以下続く
  ...
]
```

このデータをもとに、入れ子の目次を作っていきますが、この記事はテスト容易性が主のため、目次実装パートはサクサクといきます。

## 入れ子の目次を作成

目次のHTML構造は以下のイメージで、順序付きリストのアイテムは「大見出し」と「小見出しリスト」を持ちます。「小見出しリスト」はない場合もあります。

```html
<div>
  <h3>目次</h3>
  <ol>
    <li>
      <!-- ↓↓↓↓↓↓ 「大見出し」と「小見出しリスト」を持っている -->
      <a>大見出し1</a>
      <ul>
        <li><a>小見出し1</a></li>
      </ul>
     <!-- ↑↑↑↑↑↑ -->
    </li>
    <li>
      <a>大見出し2</a>
      <ul>
        <li><a>小見出し2-1</a></li>
        <li><a>小見出し2-2</a></li>
      </ul>
    </li>
    <li>
      <!-- ↓↓↓↓↓↓ 「大見出し」だけ -->
      <a>大見出し3</a>
　　　　<!-- ↑↑↑↑↑↑ -->
    </li>
  </ol>
</div>
```

つまり、microCMSからのデータを「大見出し」と「小見出しリスト」に分ければ実装できそうです。
そのように型を定義しておきましょう。
```typescript
type MainHeader = {
  fieldId: "mainHeader";
  text: string;
};

type SubHeader = {
  fieldId: "subHeader";
  text: string;
};

// 大見出しと小見出しリストを保持するオブジェクト
type tableOfContent = {
  mainHeader: MainHeader;
  subHeaders: SubHeader[];
};
```

それでは、目次コンポーネントを実装していきます。microCMS からのデータを理想の形に変えて、目次HTMLを構築しています。
(ここでは、何かごちゃごちゃして、データ整形しているな程度の理解で大丈夫です)
```react: TableOfContents
export const TableOfContents: React.FC<Props> = (props) => {
  let firstHeader: Header;
  let headers: Header[];
  [firstHeader, ...headers] = props.headers;

  if (firstHeader?.fieldId !== "mainHeader") {
    throw new Error("最初の見出しは大見出しとして下さい");
  }

  let tableOfContent: tableOfContent = {
    mainHeader: firstHeader,
    subHeaders: [],
  };

  let tableOfContents: tableOfContent[] = [];
  headers.forEach((header) => {
    if (header.fieldId === "mainHeader") {
      tableOfContents.push(tableOfContent);
      tableOfContent = { mainHeader: header, subHeaders: [] };
    } else if (header.fieldId === "subHeader") {
      tableOfContent.subHeaders.push(header);
    }
  });
  tableOfContents.push(tableOfContent);

  return (
    <>
      <div>
        <h3>目次</h3>
        <ol>
          {tableOfContents.map((toc) => (
            <MainHeaderListItem key={toc.mainHeader.text} header={toc.mainHeader}>
              <SubHeaderList headers={toc.subHeaders} />
            </MainHeaderListItem>
          ))}
        </ol>
      </div>
    </>
  );
};
```
さて、ここで気になるのは、本当にデータが正しく整形されているかです。

目次に大見出しがない場合は本当にエラーになるのか。逆にそれ以外はエラーにならないか。
小見出しだけがない場合は問題ない?
大見出しが1つだけの場合は？
...

色々不安がよぎります。

さて、ここからが本題です。

## テストしずら過ぎ問題

テストをしたいが `Props` のデータを変えて、毎回正しい `HTML` が作成されていることを確認するのは、かなり手間ですし、例えば、`<h3>目次</h3>` を `<h2>` に変えるという変更が入った場合、全てのテストを修正する必要があります。これは楽しくありません。`data-testid`属性を使って、正しいテキストが存在することなどは確認できますが、まずは、データ整形が正しい事に焦点をあてたテストが実施したいです。

そこで、出てくるのが `Container / Presentational パターン` です。
これは、関心の分離を実施するパターンです。(パターンでは、よく出てきますよね。関心の分離)



`プレゼンテーションコンポーネント`: UIを責務として、どのように表示されるかを担う
`コンテナコンポーネント`: ロジックを責務として、どのようなデータをプレゼンテーションに渡すかを担う

今回の例をこのパターンに適用してみます

```react: TableOfContents.tsx
export const TableOfContents: React.FC<Props> = (props) => {
  //ロジックの部分は同じ
  ...

  // データをプレゼンターに渡す
  return <Presenter tableOfContents={tableOfContents} />;
};
```

```react:Presenter.tsx
export const Presenter: React.FC<{ tableOfContents: tableOfContent[] }> = ({ tableOfContents }) => {
  // html の部分は同じ
  return (
    ...
  );
};
```

ただ、コンポーネントをロジック(コンテナ)と表示(プレゼンター)で分けて、コンテナからプレゼンターにデータを渡しただけです。では、テストを見てみましょう。
今回は「データが正しく整形されているか」を確認したいので、プレゼンターはモックとします。そうすることにより、素敵なことが起きます。

以下のテストの `expect` の行を見て頂ければ分かると思いますが、データ整形が正しく行われているかのテストが出来ています!
INで入ってきたデータがOUTで正しい形になっているかのテストが出来ています!!

```react:jest
jest.mock("../Presenter", () => ({
  Presenter(props: tableOfContent[]) {
    mockPresenter(props);
    return <div>モック</div>;
  },
}));

afterEach(() => {
  jest.clearAllMocks();
});

test("大見出しとそれに紐づく小見出しとして分割されること", () => {
  const headers: Header[] = [
    { fieldId: "mainHeader", text: "大見出し1" },
    { fieldId: "subHeader", text: "小見出し1-1" },
    { fieldId: "mainHeader", text: "大見出し2" },
    { fieldId: "subHeader", text: "小見出し2-1" },
    { fieldId: "subHeader", text: "小見出し2-2" },
    { fieldId: "mainHeader", text: "大見出し3" },
  ];

  render(<TableOfContents headers={headers} />);

  expect(mockPresenter).toHaveBeenCalledWith({
    tableOfContents: [
      {
        mainHeader: { fieldId: "mainHeader", text: "大見出し1" },
        subHeaders: [{ fieldId: "subHeader", text: "小見出し1-1" }],
      },
      {
        mainHeader: { fieldId: "mainHeader", text: "大見出し2" },
        subHeaders: [
          { fieldId: "subHeader", text: "小見出し2-1" },
          { fieldId: "subHeader", text: "小見出し2-2" },
        ],
      },
      { mainHeader: { fieldId: "mainHeader", text: "大見出し3" }, subHeaders: [] },
    ],
  });
});
```

どうでしょうか。「データが正しく整形されているか」が分かりやすいテストになっていると思いませんか。
小見出しがない場合のテストも簡単にできます。
```Js:jest
test("小見出しがない場合は大見出しだけで分類されること", () => {
  const headers: Header[] = [
    { fieldId: "mainHeader", text: "大見出し1" },
    { fieldId: "mainHeader", text: "大見出し2" },
    { fieldId: "mainHeader", text: "大見出し3" },
  ];

  render(<TableOfContents headers={headers} />);

  expect(mockPresenter).toHaveBeenCalledWith({
    tableOfContents: [
      {
        mainHeader: { fieldId: "mainHeader", text: "大見出し1" },
        subHeaders: [],
      },
      {
        mainHeader: { fieldId: "mainHeader", text: "大見出し2" },
        subHeaders: [],
      },
      {
        mainHeader: { fieldId: "mainHeader", text: "大見出し3" },
        subHeaders: [],
      },
    ],
  });
});
```

これなら、どんどんテスト書けそうですね。
また、プレゼンターはモックとなっているので、プレゼンター側のHTML変更が起きても、テストを変更する必要がありません。

## 楽しく実装したい

今回、入れ子の目次を作成する過程で、`Container / Presentational パターン` の良い例だなと思ったので、この記事を書きました。記事ではテスト作成が最後でしたが、最初から`Container / Presentational パターン`で実装している場合は、テストファーストの開発も可能です。こういうデータがあれば良いという事が分かれば、その入力値と返り値のテストを書いてしまい、その後にコンテナコンポーネントを実装するという流れです。

`Container / Presentational パターン` や `TDD` が正義という事はないですが、楽しく実装していきたいとは思っています。

こちらからは以上です。