React + TypeScript を E2E テストしてみよう(Cypress 入門)

8 min read読了の目安(約7800字

はじめに

https://zenn.dev/sprout2000/articles/36346c26f98e6e

の続編です。

Cypress を用いて React チュートリアルHooks & TypeScript 化済み)を E2E テストしてみます。

最終形のコードは以下にあります。

https://github.com/sprout2000/tic-tac-toe

Cypress のインストール

cypress と TypeScript で cypress を使うために必要な型定義をインストールします。

bash
$ npm i -D cypress @types/jest

Cypress の初回起動

cypress open --browser chrome で初めて cypress を起動します。--browser オプションは環境に合わせて読み替えてください。

bash
% ./node_modules/.bin/cypress open --browser chrome

It looks like this is your first time using Cypress: 7.3.0

  √  Verified Cypress! C:\Users\zenn\AppData\Local\Cypress\Cache\7.3.0\Cypress

Opening Cypress...

Windows 環境でエラーが発生する場合には?

以下のようなエラーメッセージが表示される場合には、なんらかの理由で cypress が正しくインストールされていないと思われます。

bash
$ ./node_modules/.bin/cypress open
It looks like this is your first time using Cypress: 7.3.0

  ×  Verifying Cypress can run C:\Users\zenn\AppData\Local\Cypress\Cache\7.3.0\Cypress
    → Cypress Version: 7.3.0
Cypress failed to start.

This is usually caused by a missing library or dependency.

The error below should indicate which dependency is missing.

https://on.cypress.io/required-dependencies

If you are using Docker, we provide containers with all required dependencies installed.

下のコマンドを試してみてください。

bash
$ ./node_modules/.bin/cypress install --force

成功すると次のように表示されます。

bash
Cypress 7.3.0 is installed in C:\Users\zenn\AppData\Local\Cypress\Cache\7.3.0

Installing Cypress (version: 7.3.0)

  √  Downloaded Cypress
  √  Unzipped Cypress
  √  Finished Installation C:\Users\zenn\AppData\Local\Cypress\Cache\7.3.0

You can now open Cypress by running: node_modules\.bin\cypress open

https://on.cypress.io/installing-cypress

https://stackoverflow.com/questions/48324493/cypress-failed-to-start-on-windows

cypress open で作成されたファイルを確認

レポジトリ下に cypress フォルダと cypress.json ファイルが作成されていると思います。

bash(一部抜粋)
% tree
.
├── cypress
│   ├── fixtures
│   │   └── example.json
│   ├── integration
│   │   └── examples
│   ├── plugins
│   │   └── index.js
│   └── support
│       ├── commands.js
│       └── index.js
├── cypress.json
├── package-lock.json
└── package.json

6 directories, 26 files

この中の cypress/integration フォルダ内にテストを配置していくこととなります。

TypeScript で使えるようにする

ルートディレクトリ直下の tsconfig.json を編集します。

tsconfig.json(例)
 {
   "compilerOptions": {
     "target": "ES2015",
     "module": "ES2020",
     "moduleResolution": "Node",
     "esModuleInterop": true,
     "lib": ["DOM", "ES2020"],
     "jsx": "react",
     "strict": true,
     "allowJs": true,
+    "skipLibCheck": true,
     "resolveJsonModule": true,
     "forceConsistentCasingInFileNames": true
   },
+  "include": ["node_modules/cypress/types/*.ts", "cypress/*/*.ts"],
   "ts-node": {
     "compilerOptions": {
       "target": "ES2015",
       "module": "CommonJS"
     }
   }
 }

skipLibCheckinclude を付け加えてください。

つづいて cypress ディレクトリの下にも tsconfig.json を作成し、ルートディレクトリの tsconfig.json を継承します。

cypress/tsconfig.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "isolatedModules": false,
    "types": ["cypress"]
  },
  "include": ["./**/*.ts"]
}

https://qiita.com/yasuhiro-yamada/items/3c151f271e1df9523f15

cypress.json の設定

cypress の設定はルートディレクトリ直下に作られた cypress.json へ記述します。

cypress.json
{
  "baseUrl": "http://localhost:3300"
}

baseUrlwebpack-dev-server (もしくは何らかの開発用サーバ)で設定しているポート番号を指定してください。

NPM Scripts の登録

package.jsonscripts セクションにコマンドを指定することで npm test でテストを起動できるようにします。

package.json
  "scripts": {
    "start": "cross-env NODE_ENV=\"development\" webpack serve",
    "test": "cypress run --browser chrome --spec=./cypress/integration/App.spec.ts"
  },

とりあえずテストを動かしてみる

cypress/integration フォルダ内に App.spec.ts を作成します。

cypress/integration/App.spec.ts
describe('Tic Tac Toe', () => {
  // テストのたびにルートディレクトリへ移動します
  beforeEach(() => {
    cy.visit('/');
  });

  it('レンダリングするだけ', () => {
    // 'body' タグ内に 'Next player' という文字列が存在するか?
    cy.get('body').contains('Next player');
  });
});

npm start でアプリを起動したのちに npm test でテストを実行します。

bash
% npm start &
[1] 5210
                                                                         
> tic-tac-toe@0.2.5 start
> cross-env NODE_ENV="development" webpack serve

ℹ 「wds」: Project is running at http://localhost:3300/
ℹ 「wds」: webpack output is served from 
ℹ 「wds」: Content not from webpack is served from /Users/zenn/tic-tac-toe/public
ℹ 「wdm」: Compiled successfully.

% npm test

開発用ローカルサーバが立ち上がっていないと以下のようなエラーが表示されます。

bash
Cypress failed to verify that your server is running.
Please start this server and then run Cypress again.

テストに成功すると cypress/videos フォルダへ再現動画を作ってくれます。

反対にテストに失敗すると cypress/screenshots フォルダへその失敗した時点でのスクリーンショットが出力されます。

TicTacToe ゲームの勝者をテストする

この項では以下の記事をほぼそのまま利用させて頂いています(感謝)。

https://zenn.dev/kai/articles/cypress-e2etest

テスト用の ID をタグに追加する

九つのマス (<button />) とステータス (<div>{status}</div>) へそれぞれを特定するための ID を付与します。

button

src/index.tsx
 interface SquareProps {
   value: string | null;
   onClick: () => void;
+  id: number;
   causedWin: boolean;
 }
 
 const Square: React.VFC<SquareProps> = (props) => {
   return (
     <button
+      data-e2e={`button-${props.id}`}
       className={props.causedWin ? 'square caused-win' : 'square'}
       onClick={props.onClick}
     >
       {props.value}
     </button>
   );
 };
 
~ snip ~

 const Board: React.VFC<BoardProps> = (props) => {
   const renderSquare = (i: number) => {
     return (
       <Square
         key={i}
+        id={i}
         value={props.squares[i]}
         onClick={() => props.onClick(i)}
       />
     );
   };

status

src/index.tsx
   return (
     <div className="game">
       <div className="game-board">
         <Board
           squares={current.squares}
           onClick={(i) => handleClick(i)}
         />
       </div>
       <div className="game-info">
-        <div>{status}</div>
+        <div data-e2e="status">{status}</div>
         <ol>{moves}</ol>
       </div>
     </div>
   );

テストを書く

  1. マスを指定して
  2. クリック
  3. そのマスを再び指定して
  4. マスに入った文字列をチェック

これを5回繰り返したのち、ステータスに表示されている文字列をチェックします。

cypress/integration/App.spec.ts
describe('Tic Tac Toe のテスト', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('勝者 X', () => {
    // X: 左上
    cy.get('[data-e2e="button-0"]')
      .click()
      .get('[data-e2e="button-0"]')
      .should('have.text', 'X');
    // O: 上中
    cy.get('[data-e2e="button-1"]')
      .click()
      .get('[data-e2e="button-1"]')
      .should('have.text', 'O');
    // X: 左中
    cy.get('[data-e2e="button-3"]')
      .click()
      .get('[data-e2e="button-3"]')
      .should('have.text', 'X');
    // O: 真ん中
    cy.get('[data-e2e="button-4"]')
      .click()
      .get('[data-e2e="button-4"]')
      .should('have.text', 'O');
    // X: 左下
    cy.get('[data-e2e="button-6"]')
      .click()
      .get('[data-e2e="button-6"]')
      .should('have.text', 'X');
    // X が勝ったと表示されているか?
    cy.get('[data-e2e="status"]').should('have.text', 'Winner: X');
  });
});

テストを実行してみると・・・

さらには

公式ドキュメントに勝るものなし。

https://docs.cypress.io/guides/overview/why-cypress