Cypressでコンポーネントの単体テストをしてみる

この記事は [FastDOCTOR After Advent Calendar ](https://tech.fastdoctor.jp/entry/2023/12/29/092753)30日の記事です。

## TL;DR

Jestといったjsdom実装のコンポーネント単体テストフレームワークの代替ソリューションとして、Cypressのコンポーネントテスト機能を試しました。

## 自己紹介

Webエンジニアの藤井です。
FDでは法人事業におけるWebサービスの開発を担当しております。

## 背景

弊事業において新たなプロジェクトの発足に伴い、新規プロダクトのテスト環境を模索しておりました。
私はこれまで、Jestに内包されているjsdomを使ってコンポーネントの単体テストを行ってきました。
しかしjsdomはあくまでエミュレーターという性質上、ブラウザ上で実行するjsと同じ動作を保証するものではありません。またコンポーネントの描画を目視で確認できない点に不満を感じていました。

そこで、jsdom以外の別のコンポーネントの単体テスト方法を模索する中で、Cypressのコンポーネントテスト機能に出会いました。
Cypressのコンポーネントテスト機能は、jsdomの欠点を補うような機能が備わっており、非常に使い勝手が良いと感じました。

Cypressのコンポーネントテスト機能は、jsdomと比べて以下のメリットがあります。

- ブラウザ上でテストが実行されるためテストの信頼性が高い
- テストの目視確認が可能
- シナリオのセットアップが容易
    - Cypressはall-in-oneをコンセプトに掲げており、テストの実装までの手数が少ない
        - assertionや外部通信へのモックといった必要なものは大体ビルトインされている

そこで、今回はCypressのコンポーネントテスト機能を使って、簡単なコンポーネントの単体テストを動かしてみようと思います。

## 準備
[CypressによるNext.jsのE2EテストのサンプルをVercelが既に用意している](https://github.com/vercel/next.js/tree/canary/examples/with-cypress)ので、そちらを利用しようと思います。が該当環境は情報が古いため、最低限動かすための変更を加えたリポジトリを私の方で別途用意しました[^1]。

[^1]: 該当リポジトリは2023/12/25時点で動作を検証したものであり、今後も含めた動作を保証するものではありません

```
$ git clone git@github.com:KentFujii/with-cypress-app.git
$ npm install
$ npm run component
```

上記を実行後、下記のような画面が表示されれば準備は完了です

![Screenshot 2023-12-24 at 22.46.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68020/dba5e51c-08c4-3953-6631-ce89ba6252a6.png)

## テストの実行

実行環境にElectronを選択します。
![Screenshot 2023-12-24 at 23.07.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68020/cdd5016a-0b45-e80f-5ed9-65047b7705ca.png)

その後に実行したいspecを選択してください。
![Screenshot 2023-12-24 at 23.22.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68020/f8477b78-4836-106c-01a7-8c64e7943bdc.png)

するとテストが実行されます。またテストの対象のコンポーネントが、テストスイートと共に描画されていることがわかります。
![Screenshot 2023-12-24 at 23.37.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68020/937ff781-f8bb-45ad-5c02-ec3993c3fbad.png)

実行されたテストの内容は下記です
```about-component.cy.tsx
import AboutComponent from 'components/about-component'

describe('<AboutComponent />', () => {
  it('should render and display expected content in a Client Component', () => {
    cy.mount(<AboutComponent />)
    cy.get('h1').contains('About Page')
    cy.get('a[href="/"]').should('be.visible')
  })
})
```

### 仕組み

Cypressのコンポーネントテスト機能は、下記の通りに動作するようです。
コンポーネントはページから分離されたブラウザにレンダリングされ、そのスタイルやAPIが検証されます。

1. コンポーネントをビルドして、ブラウザにマウントしレンダリングする
    1. ビルド設定は[こちら](https://github.com/KentFujii/with-cypress-app/blob/main/cypress.config.ts#L7)にあります
    1. マウントは[こちら](https://github.com/KentFujii/with-cypress-app/blob/main/cypress/support/component-index.html)のHTMLに対し[こちら](https://github.com/KentFujii/with-cypress-app/blob/main/cypress/support/component.ts)から実行されているようです
    1. 各コンポーネントを各テストから[cy.mount()](https://github.com/KentFujii/with-cypress-app/blob/main/cypress/component/about-component.cy.tsx#L11)でマウントし、レンダリングが実行されます
1. コンポーネントの要素を特定して、操作や検証を行う
    1. `find()`コンポーネントの要素を特定する
    1. `get()`コンポーネントの属性やプロパティを取得する
    1. `click()`指定したDOMをクリックする
    1. 詳しくはドキュメントの[Queries](https://docs.cypress.io/api/table-of-contents#Queries)や[Actions](https://docs.cypress.io/api/table-of-contents#Actions)を参照
1. 検証結果が期待通りであるかどうかを判断する
    1. `should()`検証を行う
    1. 詳しくはドキュメントの[Assertions](https://docs.cypress.io/api/table-of-contents#Assertions)を参照

## まとめ

Cypressのコンポーネントテスト機能は、jsdomの欠点を補うような機能が備わっており、非常に使い勝手が良いと感じました。テストを用意し動かすまでの手数が少ないのも魅力的です。

繰り返しになりますが、以下の点がメリットとして挙げられます。

- ブラウザ上でテストが実行されるためテストの信頼性が高い
- テストの目視確認が可能
- シナリオのセットアップが容易
    - Cypressはall-in-oneをコンセプトに掲げており、テストの実装までの手数が少ない

もしjsdom以外のコンポーネントテスト方法を模索しているのであれば、Cypressのコンポーネントテスト機能は検討してみる価値があると思います。

### 後日譚

実は前述の新規プロジェクトにおいてCypressの採用は見送りました。弊社の開発環境は可能な限りDockerに寄せるという方針ですが、[Cypressの仕組み自体がDockerと相性が悪い](https://github.com/cypress-io/cypress-docker-images/issues/431)ようです。別案として[@storybook/test-runner](https://storybook.js.org/docs/writing-tests/test-runner)を検討していますが、何か他に良いソリューションがあれば教えてください。