🌟

【 Next.js 12.1 + TypeScript 】next/jest と E2Eテスト の Cypress を共存させてミル貝。

2022/03/25に公開

少しのことにも、先達はあらまほしき事なり。
(ちょっとしたことにも、先達はあってほしいものである。)

―― 兼好法師『徒然草』 第五十二段[1]

この記事は何でしょうか?

TypeScript 環境で Next.js 12.1 の ユニットテスト用の next/jest プラグインと E2E テスト の Cypress を共存させてみます。
Windows 11 の WSL 環境(Ubuntu 20.04 WSLg も使用)での設定に関しても触れます。
題材としては、以前、Zenn にも書きました、React チュートリアルの三目並べを Next.js + TypeScript + Recoil の構成で作り変えたものに Jest と Cypress を追加して動かします。

https://zenn.dev/purenium/articles/nextjs-recoil-tic-tac-toe
https://github.com/mumei-xxxx/nextjs-recoil-tic-tac-toe-0

全体のコード(制作したもの)

https://github.com/mumei-xxxx/nextjs-12_1-next-jest-and-e2e-cypress

バージョン情報

Node.js 16.11.0

"next": "^12.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"recoil": "^0.6.1"

"@herp-inc/eslint-config": "^0.13.0",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
"@types/node": "^17.0.21",
"@types/react": "^17.0.40",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"cypress": "^9.5.1",
"eslint": "8.10.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^16.1.1",
"eslint-config-next": "12.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "^2.12.1",
"jest": "^27.5.1",
"prettier": "^2.5.1",
"start-server-and-test": "^1.14.0",
"typescript": "^4.6.2"

記事を書いた動機。概要。

先日(2022年2月18日)、Next.js 12.1 で単体テスト(Jest)に関する新機能が追加されました。

Zero-configuration Jest plugin
https://nextjs.org/blog/next-12-1#zero-configuration-jest-plugin

Setting up Jest (with the Rust Compiler)
https://nextjs.org/docs/testing#setting-up-jest-with-the-rust-compiler

これにより、Jest の設定がいままでよりシンプルにできるようになりました。
Zenn でもすでに解説されている記事があります。

先行記事
Next.js 12でJestの設定がかなり楽になった

https://zenn.dev/miruoon_892/articles/e42e64fbb55137

Vercel のこの Zero-configuration Jest plugin の発表をみて思ったのは、

「TypeScript で この next/jestプラグインとE2Eテストの Cypress と組み合わせてうまく動作するのだろうか」

ということでした。
以前、仕事で、TypeScript 環境で、Jest と Cypress を共存させる設定で苦労した記憶があるからです。

今回、改めて、Next.js 12.1 の next/jestプラグインと Cypress を共存させるという技術調査をした結果、以下の技術的課題が浮き彫りとなりました。

解決が必要な課題

  • Windows 11 での WSL2、WSLg の Ubuntu 20.04 で Cypress の GUI を動作させるための設定
  • Jest と Cypress の 競合問題。
    • キーワードが重複していることによる TypeScript(tsconfig.json) ESLint(.eslintrc.json)の設定の問題

一点ごとに解説していきます。

WSL2上で Cypress を動作させようとして発生するエラー

(※詳細は後で記載いたします。)
WSL 環境で、Cypress をインストールして、cypress open を実行します。
(下では、package.jsonyarn run cy:opencypress open が実行されるようにしています。)
すると、以下のようなエラーが発生します。

yarn run cy:open
yarn run v1.22.17
$ cypress open
It looks like this is your first time using Cypress: 9.5.1


Cypress failed to start.

This may be due to a missing library or dependency. https://on.cypress.io/required-dependencies

Please refer to the error below for more details.

----------

/home/mumei/.cache/Cypress/9.5.1/Cypress/Cypress: error while loading shared libraries: libxshmfence.so.1: cannot open shared object file: No such file or directory

----------

Platform: linux-x64 (Ubuntu - 20.04)
Cypress Version: 9.5.1
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

このエラーに関して、二点、問題があります。

  • WSL 上で GUI を動かす問題
  • Cypress に依存しているパッケージ問題

一点目。
Cypress は GUI を内包しています。
わかりやすい GUI で 直観的に、E2E テストを行うこともできます。
しかし、WSL で、GUI ツールである、Cypress を実行することは以前は困難でした。
詳細な手順は以下の記事にあります。

https://blog-jp.richardimaoka.net/20210411

ところが、喜ばしいことに、Windows 11 で Linux GUI アプリケーションを動作させるための WSLg というシステムが登場しました。
今回は、Windows 11 の WSLg を利用して、Cypress の GUI を動作させてみます。

二点目、Cypress に依存しているパッケージ問題については、Cypress 公式を参考に、Ubuntu で必要なパッケージをインストールします。

Jest と Cypress の 競合問題。

Jest と Cypress を共存させる例については、Cypress 公式に例があります。

cypress-and-jest-typescript-example

https://github.com/cypress-io/cypress-and-jest-typescript-example

上の cypress-and-jest-typescript-example の README にも書かれていますが、
Jest と Cypress は競合している部分があります。
そのため、TypeScript 環境で二つを共存させるときに、工夫が必要になります。

両者の何が競合しているのでしょうか。
具体的には、 expect などのキーワードが競合しています。
名前は同じ expect なのですが、中身は別物なのです。
試しに、Visual Studio Code で Jest の expect をホバーすると、以下のような型情報が表示されます。

src/__tests__/useCases/calculateWinner.spec.ts
const expect: jest.Expect
<SquareValueType>(actual: SquareValueType) => jest.JestMatchers<SquareValueType>

Cypress の expect をホバーすると、以下のような型情報が表示されます。

const expect: Chai.ExpectStatic
(val: any, message?: string | undefined) => Chai.Assertion

Visual Studio Code で見てもわかる通り、Jest の expect は、Jest の expect ですが、
Cypress の expect は Chai ベースであることがわかります。
名前は同じですが、別物であることがわかります。

このようなキーワードの競合があるため、Jest と Cypress で型設定(tsconfig.jsonなど)は分離する必要があります。
また、それに伴い、ESLint の設定も分離させる必要があります。(テストコードも Lint の対象にする場合)

最終的なディレクトリ構成

Jest と Cypress の 競合問題があるため、 Jest のテストコードと Cypress のテストコードはディレクトリを分けます。
Cypress は <rootDir>/cypress配下、Jest は、<rootDir>/src/__tests__ 配下にファイルを配置します。
最終的なディレクトリ構成は以下のようになりました。

最終的なディレクトリ構成
.
├── cypress
│   ├── fixtures
│   │   ├── example.json
│   │   ├── profile.json
│   │   └── users.json
│   ├── integration
│   │   ├── 1-getting-started
│   │   │   └── todo.spec.ts
│   │   ├── 2-advanced-examples
│   │   │   ├── actions.spec.ts
│   │   │   ├── ……(以下 サンプルファイル 略)……
│   │   └── tic-tac-toe
│   │       └── tic-tac-toe.spec.ts(Cypress E2Eテスト)
│   ├── plugins
│   │   └── index.js
│   ├── screenshots
│   ├── support
│   │   ├── commands.js
│   │   └── index.js
│   ├── tsconfig.json
│   └── videos
│       └── tic-tac-toe
│           └── tic-tac-toe.spec.ts.mp4
├── cypress.json
├── jest.config.js
├── LICENSE
├── next.config.js
├── next-env.d.ts
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   └── vercel.svg
├── README.md
├── src
│   ├── components
│   │   ├── Board.tsx
│   │   ├── GameHistory.tsx
│   │   ├── Game.tsx
│   │   └── Square.tsx
│   ├── infrastructure
│   │   └── recoil
│   │       ├── historyAtom.ts
│   │       ├── index.ts
│   │       ├── stepNumberState.ts
│   │       └── xIsNextAtom.ts
│   ├── pages
│   │   ├── api
│   │   │   └── hello.tsx
│   │   ├── _app.tsx
│   │   └── index.tsx
│   ├── styles
│   │   ├── globals.css
│   │   └── Home.module.css
│   ├── __tests__
│   │   └── useCases
│   │       └── calculateWinner.spec.ts(Jest のテスト)
│   ├── @types
│   │   └── global.d.ts
│   └── useCases
│       └── calculateWinner.ts
├── tsconfig.json
└── yarn.lock

Jest の設定

この部分は、先行記事と重複する部分があります。
Next.js 12.1 の next/jest を利用するために、Next.js を Upgrade します。

Terminal
yarn upgrade next

一応、package.json で、バージョンが12.1になったことを確認しておきます。
続いて、Jest のインストールです。

Terminal
yarn add -D jest @testing-library/react @testing-library/jest-dom

jest.config.js の設定。

Next.js 12.1 の next/jest を利用した Jest の設定です。
Jest と Cypress の競合問題があるため、Jest を実行するルート(ここでは、roots: ['<rootDir>/src'])を明記します。
これは、Jest が、<rootDir>/cypress 配下の Cypress のテストコードを無視するようにするためです。
また、tsconfig.json のエイリアス設定に合わせて、エイリアスの設定もします。

jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './'
})

// Add any custom config to be passed to Jest
const customJestConfig = {
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
+  roots: ['<rootDir>/src'],
  moduleDirectories: ['node_modules', '<rootDir>/'],
  // エイリアスの設定。
  moduleNameMapper: {
    '^@/(.+)$': '<rootDir>/src/$1'
  },
  testEnvironment: 'node'
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)

また、jest.config.js は TS 化するとうまく動作しませんでした。
(なにか設定があるかもしれませんが……)
ESLint にもひっかかるので、今回は、ignore しました。

.eslintrc.json
{
  "extends": ["next/core-web-vitals", "airbnb-typescript", "@herp-inc", "prettier"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json",
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint", "import"],
+ "ignorePatterns": ["jest.config.js"],
  "rules": {
    "import/no-unassigned-import": ["error", { "allow": ["**/*.css"] }]
  }
}

ルートの tsconfig.json の設定。

ここでも、Jest と Cypress のキーワードの競合問題を考慮する必要があります。

  • <rootDir>/src 配下の Jest のテストコードの型エラーの回避
    • これは、グローバルに定義されている Cypress の型情報が、Jest のキーワードに適用されることによって生じます。
  • Cypress E2E テストコードの <rootDir>/cypress 配下のファイルをコンパイル対象から除外。

一点目、<rootDir>/src 配下の Jest のテストコードの型エラーについて。
下記キャプチャは tsconfig.json 調整前の Jest のテストコードです。
Jest の toBe アサーションについて「プロパティ 'toBe' は型 'Assertion' に存在しません。」というエラーが発生しています。
本来は expect に Jest の型が適用されるべきところに、Chai の型が適用されてしまった結果、エラーが生じてしまいました。

ここは、Jest の型を適用する必要があります。
そこで、"types": ["jest"]を設定し, include で、コンパイル対象に <rootDir>/src 配下を指定しました。
これにより、<rootDir>/src 配下に Jest の型情報が適用され、上記のエラーは解消します。
そして、コンパイル対象でない Cypress の E2E テストコードがコンパイル対象から外れます。

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
+   "types": ["jest"],
  },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules"]
}

ここまで、設定して、一旦、Jest で簡単なテストを書いて動かしてみます。
テスト対象は、三目並べ勝者判定の関数 calculateWinner です。
package.jsonyarn jest で Jest のテストが実行されるように、設定します。
src/__tests__/useCases/calculateWinner.spec.ts にテストを書いていきます。

package.json
 "scripts": {
   "dev": "next dev",
   "build": "next build",
   "start": "next start",
+  "jest": "jest",
   ……(略)……
 },
src/__tests__/useCases/calculateWinner.spec.ts
import { calculateWinner } from '@/useCases/calculateWinner'

describe('三目並べ勝者判定の関数 calculateWinner', (): void => {
  test('勝者なし(引き分け)', (): void => {
    const winner = calculateWinner(['X', 'X', 'O', 'O', 'O', 'X', 'X', 'O', 'X'])
    expect(winner).toBeNull()
  })
  test('上横一列で先手(X)勝ち。', (): void => {
    const winner = calculateWinner(['X', 'X', 'X', 'O', 'O', null, null, null, null])
    expect(winner).toBe('X')
  })
  test('下横一列で後手(O)勝ち。', (): void => {
    const winner = calculateWinner(['X', null, null, 'X', 'X', null, 'O', 'O', 'O'])
    expect(winner).toBe('O')
  })
})

yarn jest でテストを実行すると、以下のように Jest のテストが成功しました。

Terminal
yarn jest
yarn run v1.22.17
$ jest
 PASS  src/__tests__/useCases/calculateWinner.spec.ts
  三目並べ勝者判定の関数 calculateWinner
    ✓ 勝者なし(引き分け) (3 ms)
    ✓ 上横一列で先手(X)勝ち。
    ✓ 下横一列で後手(O)勝ち。

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.2 s, estimated 1 s
Ran all test suites.
Done in 1.26s.

Cypress の設定。

Windows 11 で WSLg を利用できるようにする。

これに関しては、別に記事に解説を譲ります。
例えば、以下の記事などです。

https://www.teamxeppet.com/wsl2-win11-install/

動作確認として、xeyes というアプリをインストールします。

Terminal
sudo apt install x11-apps

コマンド

Terminal
xeyes 

の実行で、gif のように、GUI が立ち上がり、カーソルの動きに合わせて目玉が動けば OK です。

Cypress 依存パッケージのインストール

以下の Cypress のドキュメントを参考に、依存パッケージをインストールします。

Dependencies Introduction | Cypress Documentation

https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies

その前に、Ubuntu のパッケージリストの更新をします。

Terminal
sudo apt-get update
sudo apt-get upgrade

筆者の環境は、 Ubuntu なので、Ubuntu で必要なパッケージをインストールします。
コマンドは、ここにも記載しますが、公式の最新のドキュメントで必要とされているものをインストールしていただければと思います。

Terminal
apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb

Cypress のインストール

ここで、Cypress をインストールします。

Terminal
yarn add cypress --dev

正常にインストールされたか確認します。

Terminal
npx cypress verify

✔  Verified Cypress! /home/XXXX/.cache/Cypress/9.5.1/Cypress

「✔ Verified Cypress!」というメッセージがでれば、OK です。
Cypress は正常にインストールされました。

Cypress の初期化

cypress open で Cypress の初期設定をします。
package.jsonyarn run cy:opencypress open が実行されるように設定します。

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "lint:fix": "eslint src --config .eslintrc.json --ext .js,jsx,.ts,.tsx --fix",
    "lint:cypress": "eslint cypress --config cypress/.eslintrc.json --ext .js,jsx,.ts,.tsx --fix",
    "prettier": "prettier --write --ignore-path .gitignore './**/*.{js,jsx,ts,tsx,json,css}'",
+   "cy:open": "cypress open",
    "test": "jest",
    "format": "yarn run prettier && yarn run lint:fix"
  },

yarn run cy:openを実行すると、以下のような GUI が立ち上がれば成功です。

tsconfig.json に依存した ESLint エラー

ここで、cypress/integration 配下で大量のエラーが発生します。

Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: cypress/integration/1-getting-started/todo.spec.js.
The file must be included in at least one of the projects provided.eslint

こちらのエラーメッセージの意味は、

.eslintrc.jsonparserOptionsproject で、tsconfig.json を読み込んでますよね?
で、その対象ファイルの設定で、
"include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"],
でなってますよね?
でも、このファイルは、cypress/integration/1-getting-started/todo.spec.js
で、上の、「"next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"」 のどのパターンとも合致しないんですけど?』

ということになります。
したがって、実際のファイル構成(新規に追加された Cypress の JavaScript ファイル群)と、.eslintrc.jsonparserOptionsproject の対象ファイルの設定を合致させる必要があります。

ここでいろいろな方針が考えられますが

今回は、

  • 新規に追加する Cypress のテストコードも TypeScript で書き、ESLint の対象に含める。
    • (見本用に残しておくテストコード、ボイラープレートの JS は、ESLint の ignorePatterns で妥協。)

とします。
これは、テストコードにも TypeScript の型チェックと ESLint を効かせ、テストコードの内部品質を保つ意図です。
また、これにより、.eslintrc.json の設定もシンプルになります。

そして、cypress open のコマンドで実行した際に、生成された見本のテストコードは、一旦は、動作確認用として残しておくことにします。
これらの JavaScript コードを TypeScript 化していきます。(拡張子を.jsから.tsに変えていきます。)
当プロジェクトでは以下のファイル群です。

  • cypress/integration/1-getting-started 配下
  • cypress/integration/2-advanced-examples 配下

'--isolatedModules' のエラー

すると、今度は、以下のようなエラーが出ます。

'todo.spec.ts' は、グローバル スクリプト ファイルと見なされるため、
'--isolatedModules' でコンパイルすることはできません。
import、export、または空の 'export {}' ステートメントを追加して、
これをモジュールにしてください。ts(1208)

'--isolatedModules' でコンパイルすることはできません。」とメッセージがでます。
これについて下記のページに解説があります。ここでも簡単に意味を解説いたします。

ts-jestでcannot be compiled under '--isolatedModules'と出た時の対処法

https://zenn.dev/ryo_kawamata/articles/0f63b7ffdaed97

このエラーが起こる原因は、tsconfig.jsonisolatedModules オプションが true になっているからです。
isolatedModules については、以下に公式のリンクを貼ります。

isolatedModules - TypeScript: TSConfig Reference - Docs on every TSConfig option
https://www.typescriptlang.org/tsconfig#isolatedModules

isolatedModules オプションとは、エラーメッセージ通り、「コンパイルできない」というエラーです。
どういう場合にでしょうか。
そのファイル単一で見た場合にコンパイルできない場合にです。
具体的に見ますと、例えば、
cypress/integration/1-getting-started/todo.spec.ts
のファイルを見れば、describe などの Cypress のボイラープレートがあります。
これは、Cypress 本体に定義されているものです。
したがって、この todo.spec.ts だけ単体でみても TypeScript のコンパイラはこの describe がなにか判断できません。
これが、「'--isolatedModules' でコンパイルすることはできません。」の意味です。

cypress/tsconfig.json、cypress/.eslintrc.json の追加

さて、現在、判明している問題は、

  1. 上記の Parsing error
  2. Jest と Cypress のキーワード競合
  3. isolatedModules のエラー

です。
上記にリンクを掲載した cypress-io/cypress-and-jest-typescript-example ではどうしているのかといいますと、
<rootDir>/cypress 配下に、Cypress のテストコード用の、cypress/tsconfig.json を用意しているようです。
そこで、今回は、

  • cypress/tsconfig.json
  • cypress/.eslintrc.json

と2つのファイルをおいて、TS と ESLint の設定をすることにしました。

cypress/tsconfig.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "noEmit": true,
    "isolatedModules": false,
    // be explicit about types included
    // to avoid clashing with Jest types
    "types": ["cypress"]
  },
  "include": ["../node_modules/cypress", "**/*.ts"]
}
cypress/.eslintrc.json
{
  "extends": ["next/core-web-vitals", "plugin:cypress/recommended", "airbnb-typescript", "@herp-inc", "prettier"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./cypress/tsconfig.json",
    "sourceType": "module"
  },
  // https://github.com/cypress-io/eslint-plugin-cypress
  "plugins": ["@typescript-eslint", "import", "cypress"]
}

cypress/.eslintrc.jsonを配置することにより、<rootDir>/cypress 配下では、cypress/.eslintrc.jsonの設定が優先して適用されるようになります。
また、cypress/.eslintrc.json では、parserOptions.projectcypress/tsconfig.json を参照するようにします。

  1. 上記の Parsing error について。
    拡張子を.tsにしますと、cypress/tsconfig.jsoninclude のパターンと合致するようになるで解消します。

  2. Jest と Cypress のキーワード競合
    cypress/tsconfig.jsoncompilerOptions"types": ["cypress"] を設定しました。
    これにより、<rootDir>/cypress 配下で、expect などのキーワードに明示的に、Cypress の型が適用されるようになります。
    Cypress 公式の TypeScript での設定のページを参考にしました。

https://docs.cypress.io/guides/tooling/typescript-support#Install-TypeScript

  1. isolatedModules のエラーについて。
    これは上でも述べたように、「コンパイルできない」というエラーです。
    しかし、<rootDir>/cypress 配下のテストコードは、コンパイルの必要がありません。
    なので、cypress/tsconfig.json"isolatedModules": false を設定すれば、isolatedModules のエラーは解消します。

その他、
Cypress 公式が作っている、Cypress ESLint Plugin を今回見つけたので、 ESLint 設定を入れてみました。

Cypress ESLint Plugin

https://github.com/cypress-io/eslint-plugin-cypress

インストール

yarn add eslint-plugin-cypress --dev

設定の仕方は、公式の README や上記の cypress/.eslintrc.json を見ていただければと思います。

package.json で ESLint コマンド追加、動作確認

続いて、package.json で ESLint 用のコマンドを修正します。

  • yarn lint:fix<rootDir>/src 配下の Lint を ルートの .eslintrc.json の設定
  • yarn lint:fixCypress<rootDir>/cypress 配下の Lint を cypress/.eslintrc.json の設定

で行うようにします。

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
-   "lint:fix": "eslint src --ext .js,jsx,.ts,.tsx --fix",
+   "lint:fix": "eslint src --config .eslintrc.json --ext .ts,.tsx --fix",
+   "lint:fixCypress": "eslint cypress --config cypress/.eslintrc.json --ext .ts,.tsx --fix",
    "prettier": "prettier --write --ignore-path .gitignore './**/*.{js,jsx,ts,tsx,json,css}'",
    "cy:open": "cypress open",
    "jest": "jest",
-   "format": "yarn run prettier && yarn run lint:fix"
+   "format": "yarn run prettier && yarn run lint:fix && yarn run lint:fixCypress"
  },

ここで、yarn lint:fixCypress を実行して、ESLint の動作確認をしてみます。

Terminal
yarn lint:fixCypress
yarn run v1.22.17
$ eslint cypress --config cypress/.eslintrc.json --ext .ts,.tsx --fix
()
/home/XXXXX/ZZZZZZZZ/cypress/integration/2-advanced-examples/viewport.spec.ts
  29:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  31:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  33:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  35:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  37:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  39:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  41:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  43:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  45:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  47:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  52:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  54:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting

/home/XXXXX/ZZZZZZZZ/cypress/integration/2-advanced-examples/waiting.spec.ts
  13:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  15:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting
  17:5  error  Do not wait for arbitrary time periods  cypress/no-unnecessary-waiting

✖ 94 problems (94 errors, 0 warnings)

正常に、<rootDir>/cypress 配下への ESLint が動作していることが確認できました。
新規に追加した Cypress ESLint Plugin も効いていることも確認できました。
ただし、今回は、上に書いたように、見本用に残しておくテストコード、ボイラープレートの JS 等は、ESLint の ignorePatterns で ignore します。

cypress/.eslintrc.json
{
  "extends": ["next/core-web-vitals", "plugin:cypress/recommended", "airbnb-typescript", "@herp-inc", "prettier"],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./cypress/tsconfig.json",
    "sourceType": "module"
  },
+ "ignorePatterns": ["support", "plugins", "integration/1-getting-started", "integration/2-advanced-examples"],
  // https://github.com/cypress-io/eslint-plugin-cypress
  "plugins": ["@typescript-eslint", "import", "cypress"]
}

これで、TypeScript(tsconfig.json) ESLint の .eslintrc.json の設定は一旦終わりです。

Cypress で実際にテストを動かす。

まず、E2E テストのためには、テストコード側で DOM を取得する必要があります。
これには、Cypress 公式にベストプラクティスがあります。
それは、テスト用にdata-cyなどのデータ属性を設定することです。
下準備としてこれを行います。

src/components/Square.tsx(必要個所を抜粋)
export const Square: React.FC<SquarePropsType> = ({ value, squareIndex, onClick }) => {
  return (
-   <button className="square" onClick={onClick}>
+   <button className="square" onClick={onClick} data-cy={`square_${squareIndex}`}>
      {value}
    </button>
  )
}
src/components/GameHistory.tsx(必要個所を抜粋)
return (
  <div className="game-info">
-   <div>{status}</div>
+   <div data-cy="winner_status">{status}</div>
    <ol>{moves}</ol>
  </div>
)

デベロッパーツールでみると下のような感じになります。

ここで設定したデータ属性で E2E テストを書いていきます。

cypress/integration/tic-tac-toe/tic-tac-toe.spec.ts
/**
 * @description 三目ならべの盤面のコンポーネント
 */
describe('三目ならべ', () => {
  beforeEach(() => {
    cy.visit('http://localhost:3000/')
  })

  it('次の手番は、先手(X)', () => {
    cy.get('[data-cy=square_0]').click()
    cy.get('[data-cy=square_1]').click()
    cy.get('[data-cy=winner_status]').should('have.text', 'Next player: X')
  })
  it('次の手番は、後手(O)', () => {
    cy.get('[data-cy=square_0]').click()
    cy.get('[data-cy=winner_status]').should('have.text', 'Next player: O')
  })
  it('先手(X)上横一列勝ち', () => {
    cy.get('[data-cy=square_0]').click()
    cy.get('[data-cy=square_3]').click()
    cy.get('[data-cy=square_1]').click()
    cy.get('[data-cy=square_4]').click()
    cy.get('[data-cy=square_2]').click()
    cy.get('[data-cy=winner_status]').should('have.text', 'Winner: X')
  })
  it('後手(O)下一列勝ち', () => {
    cy.get('[data-cy=square_0]').click()
    cy.get('[data-cy=square_6]').click()
    cy.get('[data-cy=square_3]').click()
    cy.get('[data-cy=square_7]').click()
    cy.get('[data-cy=square_1]').click()
    cy.get('[data-cy=square_8]').click()
    cy.get('[data-cy=winner_status]').should('have.text', 'Winner: O')
  })
})

ここで、yarn dev などで、サーバーを立ち上げた状態で、yarn cy:open して、テストを実行します。
正常にテストが行われていることが確認できました。

(※) WSL Ubuntu で Cypress を実行して日本語が文字化けする場合

上記のテストの実行で、日本語が「□□□」のように、うまく表示されない場合があります。
「豆腐フォント」とも言われる現象です。
その場合は、Ubuntu の日本語の設定をします。

Terminal
sudo apt-get update
sudo apt-get install -y locales locales-all
sudo apt-get remove fonts-vlgothic
sudo apt-get install -y fonts-vlgothic
sudo locale-gen ja_JP.UTF-8
sudo localedef -f UTF-8 -i ja_JP ja_JP.utf8

CI を見据えた cypress run の設定。(start-server-and-test 使用)

AWS Code build などで Cypress で E2E テストを行うとき、

  • サーバー( http://localhost:3000/ 等)を起動する。
  • サーバーの起動したら、Cypress で E2E テストを行う。

という手順を得る必要があります。
それを実現するのが、start-server-and-test というライブラリです。
Cypress 公式でも紹介されています。

https://github.com/bahmutov/start-server-and-test

https://docs.cypress.io/guides/continuous-integration/introduction#Boot-your-server

三目ならべのみをテストしたい場合は以下のようになります。

package.json
  "scripts": {
    ……
+   "ci:cy": "start-server-and-test 'yarn build && yarn start' http://localhost:3000 'cypress run --spec 'cypress/integration/tic-tac-toe/**''",
    ……
  },

実際にコマンド実行してみると、

  • Next.js をビルドしたあと、
  • サーバーを立ち上げ、
  • その後に、三目ならべの E2E テストを行う、

という挙動が実現できているということを確認できました。
start-server-and-test の詳細な使用方法については、上記に掲載した start-server-and-test 公式などの解説をご参照いただければ幸いです。

Jest と Cypress のテストを連続して実行。

最後に、Jest のユニットテストと Cypress の E2E テストを連続して実行してみます。
yarn test コマンドで行います。

package.json
  "scripts": {
    ……
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "jest": "jest",
    "ci:cy": "start-server-and-test 'yarn build && yarn start' http://localhost:3000 'cypress run --spec 'cypress/integration/tic-tac-toe/**''",
+   "test": "yarn jest && yarn ci:cy",
    ……
  },

実行すると以下のような形となり成功しました。
(一部フォルダ名を変更。)

「 yarn test 」の実行結果を click で展開します。
Terminal
yarn test
yarn run v1.22.17
$ yarn jest && yarn ci:cy
$ jest
 PASS  src/__tests__/useCases/calculateWinner.spec.ts
  三目並べ勝者判定の関数 calculateWinner
    ✓ 勝者なし(引き分け) (10 ms)
    ✓ 上横一列で先手(X)勝ち。
    ✓ 下横一列で後手(O)勝ち。 (1 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.872 s, estimated 1 s
Ran all test suites.
$ start-server-and-test 'yarn build && yarn start' http://localhost:3000 'cypress run --spec 'cypress/integration/tic-tac-toe/**''
1: starting server using command "yarn build && yarn start"
and when url "[ 'http://localhost:3000' ]" is responding with HTTP status code 200
running tests using command "cypress run --spec cypress/integration/tic-tac-toe/**"

$ next build
info  - Checking validity of types
info  - Creating an optimized production build
info  - Compiled successfully
info  - Collecting page data
info  - Generating static pages (3/3)
info  - Finalizing page optimization

Page                                       Size     First Load JS
┌ ○ /                                      6.06 kB        99.2 kB
├   └ css/4fbf8bdb1226e764.css             668 B
├   /_app                                  0 B            93.2 kB
├ ○ /404                                   194 B          93.4 kB
└ λ /api/hello                             0 B            93.2 kB
+ First Load JS shared by all              93.2 kB
  ├ chunks/framework-e70c6273bfe3f237.js   42 kB
  ├ chunks/main-a054bbf31fb90f6a.js        27.6 kB
  ├ chunks/pages/_app-fde71f0e06f79000.js  22.7 kB
  ├ chunks/webpack-69bfa6990bb9e155.js     769 B
  └ css/79aa2f09e4a3fce4.css               421 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)

$ next start
ready - started server on 0.0.0.0:3000, url: http://localhost:3000

====================================================================================================

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:        9.5.1                                                                          │
  │ Browser:        Electron 94 (headless)                                                         │
  │ Node Version:   v16.11.0 (/home/XXXX/.nodenv/versions/16.11.0/bin/node)                       │
  │ Specs:          1 found (tic-tac-toe/tic-tac-toe.spec.ts)                                      │
  │ Searched:       cypress/integration/tic-tac-toe/tic-tac-toe.spec.ts                            │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


────────────────────────────────────────────────────────────────────────────────────────────────────

  Running:  tic-tac-toe/tic-tac-toe.spec.ts                                                 (1 of 1)


  三目ならべ
    ✓ 次の手番は、先手(X) (713ms)
    ✓ 次の手番は、後手(O) (215ms)
    ✓ 先手(X)上横一列勝ち (527ms)
    ✓ 後手(O)下一列勝ち (640ms)


  4 passing (3s)


  (Results)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Tests:        4                                                                                │
  │ Passing:      4                                                                                │
  │ Failing:      0                                                                                │
  │ Pending:      0                                                                                │
  │ Skipped:      0                                                                                │
  │ Screenshots:  0                                                                                │
  │ Video:        true                                                                             │
  │ Duration:     3 seconds                                                                        │
  │ Spec Ran:     tic-tac-toe/tic-tac-toe.spec.ts                                                  │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


  (Video)

  -  Started processing:  Compressing to 32 CRF
  -  Finished processing: /home/XXXX/XXXXX/cypress/videos/tic-tac-toe/tic-tac-toe.spec.ts.mp4


====================================================================================================

  (Run Finished)


       Spec                                              Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  tic-tac-toe/tic-tac-toe.spec.ts          00:03        4        4        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✔  All specs passed!                        00:03        4        4        -        -        -

Done in 78.96s.

これで当初の目標である、

Next.js 12.1 の next/jest プラグインと E2E テスト の Cypress を共存させてみる
(Windows 11 の WSL 環境(WSLg も使用)で)
が実現できました。

結語

この記事を執筆するにあたって、複数のテストに関する記事を参照しました。
「E2E テストは最小限にする」という趣旨のものが多かったです。(例えば新しいものでは下記の記事です)
ただ、E2E テスト自体を否定しているようなものは、私の観測範囲では、なかったです。

この記事は、私が遭遇したエラーの備忘録としての面があります。
なので、記述がまどろっこしい部分もあるかもしれませんが、なにかのヒントになれば幸いに存じます。

https://zenn.dev/mizchi/articles/my-test-policy

その他参考にした記事

Next.js プロジェクトに Cypress を導入して GitHub Actions で E2E テストをする
https://zenn.dev/a_da_chi/articles/7ba871c23c5510

Cypress on Docker で自動テストすると日本語が文字化ける
https://technology-memo.net/2019/04/05/cypress-in-japanese/

脚注
  1. 少しのことにも先達はあらまほしきことなり
    https://ameblo.jp/agri9216/entry-12366277910.html
    徒然草第52段
    https://www2.yamanashi-ken.ac.jp/~itoyo/tsuredure/turedure050_099/turedure052.htm ↩︎

Discussion