フューチャー技術ブログ

Cypress - 設定編

Cypressの設定周りについて紹介します。

何も手を加えないデフォルトでも動作します。

Cypressの設定

フォルダ構成とTypeScript化

まず、Cypressのデフォルトのフォルダ構成がこちらです。何も指定しないと、プロジェクトルートにcypressフォルダがあって、その中に関連ファイル(テストケース、プラグインなど)が置かれるのが基本パターンです。

Cypressのフォルダ構成
cypress
├── fixtures
│   └── users.json
├── integration
│   ├── textbox.html
│   └── textbox.spec.js
├── plugins
│   └── index.js
├── screenshots
├── support
│   ├── commands.js
│   └── index.js
└── videos

Cypressはnpm install cypressで必要なツールをまとめてインストールできます。

初回実行時にこのcypressフォルダとcypress.jsonが作られます。設定やテスト、ヘルパー関数などはこの雛形の中に書いていきます。

デフォルトで生成される設定ファイルはJavaScriptのコードですが、TypeScript化にも対応しています。TS化したいときは次のことを行います。

  • npm i -D typescript @types/node
  • tsconfig.jsonを作成

tsconfig.jsonは通常、プロジェクトのルートのpackage.jsonがあるフォルダに置きますが、プロジェクトのtsconfig.jsonとは別にこのフォルダ内にCypress専用tsconfig.jsonを置くこともできます。

あとはこのフォルダ内部の.js.tsに置換していけば型が書けます。

↓細かいところはこちらを参照してください。

https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/fundamentals__typescript

今時はみんなTypeScript使うでしょうし、この説明では全部TS化する前提で話を進めます。といってもJSのまま使うにはデフォルトのままで大丈夫なので、特に考慮することなくこのエントリーの説明を読み進めていけます。

テストのカスタマイズポイント

テスト時の動作のカスタマイズ項目は次のページに書かれています。動画の自動のキャプチャ機能や失敗時のスクリーンショット機能をオフにしたり、テストランナーでの実行時にテストファイルの変更を監視して自動テストを行う(watch)機能を無効にしたり、フォルダ位置を変えたりといったことが変更可能です。

https://docs.cypress.io/guides/references/configuration

基本的に5種類のカスタマイズ方法があります。

  • ルートのcypress.json
  • cypress.env.json
  • 環境変数
  • CLIのオプション
  • cypress/plugins/index.ts

現在のテストが読み込んでいる設定が、どの項目から読み込まれたのかはテストランナーのSettingsメニューで確認できます。

CypressテストランナーのSettingsメニュー

envの項目で設定した値は、テストコード中からアクセスできるため、テストのパラメータを外部から変更できるようにすることもできます。

フォルダ構成の詳細

最初にデフォルトのフォルダ構成を紹介しました。それぞれのフォルダの役割は次の通りです。

  • fixtures: テスト中で使いたいデータファイル置き場
  • integration: テストコード置き場
  • plugins: プラグインの登録や設定の変更
  • support: ちょっとしたコマンド追加など
  • videos: 実行中に記録された動画ファイル置き場
  • screenshots: 実行中に記録されたスクリーンショット置き場

これらのフォルダはcypress.jsonでフォルダの位置を変えたりできます。また、最初に呼び出される設定ファイル(基本はcypress/plugins/index.js、このパスも変更できる)でフォルダ構成の変更もできます。

詳しくは設定の説明を読むと良いでしょう。

https://docs.cypress.io/guides/references/configuration

cypress/support/index.ts

npm installしたプラグインのうち、ただコマンドを足すだけのシンプルなものを登録したり、自作のコマンドの登録をしたりする設定置き場です。コマンドはcommands.tsに書いて、このファイルにはimport文を書いて参照する方法が一般的なようです(という構成の設定がデフォルトで作られる)。

cypress/support/index.ts
import "@testing-library/cypress/add-commands"
import "@cypress/code-coverage/support"
import "./commands"

コマンドの作り方は後述します

cypress/plugins/index.ts

Cypress内部のイベントを受け取る必要のあるプラグインの初期化や、設定の上書きなどを行います。

cypress/plugins/index.ts
/// <reference types="cypress" />

module.exports = (
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
) => {
require("@cypress/code-coverage/task")(on, config)
return {
...config,
integrationFolder: "cypress/specs",
};
})

最低限のcypress.jsonは空でもいいのですが、baseUrlだけ設定しておくと、cy.visit()とかcy.request()のパスにprefixとしてつくので、テストコードがポートやホスト名にハードコードされなくなります。ちょっと変更に強くなります。ローカルと、リモートのテストの両方で使う場合はbaseUrlを外から変えればいけるようになるので、基本的には設定しておくべきでしょう。npm run serveなどで起動するテストサーバーや、go runで起動するサーバーに向けておきます。

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

vue-cliで自動生成すると、ちょっと違うフォルダに入ります。また、vue-cliで実行した場合は、npm run serveして、そのURLをcypressに渡すところまでやってくれるので便利です。

Cypressの設定は画面からみれます。どこで設定された値なのかが一目瞭然ですごく親切。

Cypressが起動時に読み込むファイルは次の通り

  1. cypress.jsonでフォルダの場所などを読み取り
  2. pluginとsupportの読み込み(設定なければcypress/plugins/index.jsとcypress/support/index.jsなど)
  3. テストケースの読み込み(設定なければcypress/integrations)
  4. openモードでなければそのままテストを実行、openモードの場合はIDEを起動

pluginsというフォルダ名ではあるものの、設定を変えたりします。次のサンプルはコードカバレッジを有効化しつつ、テストの置き場のフォルダを変更しています。WebPackの設定を変更したりするのはこちらです。

cypress/plugins/index.ts
/// <reference types="cypress" />

module.exports = (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => {
require("@cypress/code-coverage/task")(on, config)

return {
...config,
integrationFolder: "cypress/specs",
};
})

これら以外にも、環境変数で値を渡したり、コマンドライン引数で値を設定したりといった機能があります。

Vue.js以外の実行の流れ

基本的な流れは次の通りです。

  1. E2Eテスト対象のサーバーを起動します
    ウェブサーバーやフロントエンドの開発サーバーが該当します。静的HTMLならホスティングもCypressだけでできます。4/6にリリースされた7.0ではStorybook的にReact/Vueコンポーネントのテストが
    直接書ける
  2. cypress runでヘッドレス実行、あるいは、cypress openでGUI Test Runnerを起動してテストします。
    cypress runでヘッドレス実行、あるいは、cypress openでGUI Test Runnerを起動してテストします。テストの中でcy.visit(“接続先URL”)で1で起動したテスト対象のページにアクセスします。cypress.jsonなどでbaseUrlを指定すれば、ここからの相対パスで次のようにテストをシンプルにできます。また、ローカル、stg環境など複数の環境でテストを再利用したい場合に便利です。
  • cy.visit(“http://localhost:3000/”) # 愚直に書く
  • cy.visit(“/”) # baseUrlからの相対パス
cypress.json
{
"baseUrl": "http://localhost:3000"
}

Vue.jsの実行の流れ

CypressをVue.jsのプロジェクトに導入するのは簡単です。

インストール時にE2Eを選択し、その後の選択でCypressを選択するとインストールされます。また、作成済みのプロジェクトであればvue add e2e-cypressを実行すれば追加できます。

Vueの場合、テストはすべてtest/e2e以下に格納するようになっています。

実行すると開発サーバー実行と、それをbaseUrlに設定して起動するところまでvue-cli-serviceがやってくれます。

CypressとJest

Cypressはmocha + chaiベースでテストを書きます。ユニットテストで現在一番人気はJestですが、Jestとmocha, chaiでキーワードがいろいろ衝突します。特にchaiとJestのexpectが大きいです。全然違うなら問題ないのですが、似ているようで微妙にメソッドが違ったりと、微妙な差のため、「こちらは動くのに、こちらは全然動かない」という微妙な落とし穴位になりがちです。

公式サイトで紹介されている方法はプロジェクトのルートのtsconfig.jsonと、cypress/tsconfig.jsonを使い分けて回避する方法です。まずルートの方のプロジェクトですが、プロジェクトに必要な設定と、型に”jest”を入れておきます。また、includeでCypress以外の設定も入れておきましょう。

import { expect } from '@jest/globals';
/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": ["ES2015", "DOM"],
"strict": true,
"types": ["jest"],
"noEmit": true
},
"include": ["src/*.ts"],
"exclude": ["src/*.test.ts"]
}

こちらはCypress側の設定です。extendsで親フォルダの設定を読み込み、必要な箇所だけ追加します。ここではtypesにcypressを足しています。これで”jest”との衝突を防げます。

/cypress/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"types": ["cypress"]
},
"include": [
"../node_modules/cypress",
"./**/*.ts"
]
}

CypressのMochaはGUIのテストにしか使ってはいけないわけではなく、単純なユニットテストに使っても良いといえば良いのですが、動画撮影機能とか失敗時のスクリーンショット機能のせいでシンプルなテストほどオーバーヘッドが大きい(全部オフにすれば普通になりますが)ですし、ふたつ使い分ける方が良いでしょう。

手元の環境ではJestとCypressのカバレッジを統合するのがうまくいかず、いっそのことCypressに寄せる、というのも考えたりはしたのですが、Node.js(というかElectron)の機能を使ったテストが実行できないなど、欠点もあって、それが受容できるかどうか次第ですね。

簡単に済ませるのであれば、expect()のmatcherの挙動が違うだけなので、cypressとjestを両方tsconfig.jsonに入れてしまって、明示的にimportしてあげれば2つ作らなくても対応可能です。仕事のプロジェクトではこれを書くようにしちゃっています。

コマンドの作り方

テストコードを書いていると、同じような命令が繰り返し登場することがあります。例えば、ログインを毎回行っている、特定のページに遷移する、データの登録を行ってデータがある前提のテストに備えるなどなど。

テストはなるべく構造化しないで、愚直に書いた方が良いことの方が多いのですが、準備コードが長くなりすぎるのもフォーカスがぼやけてしまってよくないです。その場合にテストコードを短くして見通しを改善する方法は主に2つあります。

  • 同一のファイルのテスト間ではbeforeEach()を使って実装をまとめる
  • ファイルを跨いだり、横断的に使う場合はカスタムコマンドを作成する

ここでは後者の方法について軽く紹介します。ただし、短くしすぎて見にくくならないようにすることが大事です。テストコードの読解のためにたくさんの関数の中身を調べないといけない、というのはよくないテストコードです。多少冗長でも読みやすくて意味が把握しやすければ問題ありません。またこの独自コマンドを活用する方法はびろうさんが詳しく紹介してくれます。また、たんなる公開APIの列挙ではない、よりCypressの内部に突っ込んだ独自コマンドの実装方法も別のエントリーで紹介します。

追加コマンドの定義箇所はsupportフォルダ以下です。

npmインストールした追加のコマンドを増やすのはsupport/index.tsに書きます。プロジェクトで作成するコマンドはここから読み込まれるcommand.tsに入れておきます。

cypress/support/index.ts
import "@testing-library/cypress/add-commands"
import "@cypress/code-coverage/support"
import "./commands"

コマンドはログインだったり、一連の動作を連続実行するのに便利な仕組みです。中ではcypressのテストを書くための一般的なAPIを呼び出します。次のコマンドはログインを一発で行うコマンドを追加した例です。

cypress/support/commands.ts
Cypress.Commands.add("login", () => {
cy.visit("/", { timeout: 10000 });
cy.url().should("match", /auth/);
cy.get("#user").type("testuser1");
cy.get("#passwd").type("testuser1-password");
cy.get("#login").click();
})

追加したコマンドは、cyオブジェクトの追加メソッドのように見えます。このままではTypeScriptから識別されず、TypeScriptのコンパイルでエラーになってしまいますし、コード補完もできませんので、型定義ファイルを用意します。

cypress/support/index.d.ts
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
login();
}
}

まとめ

Cypressの設定周りの構造と初期化周りの挙動と、コマンドの登録について簡単に紹介しました。

補足

本記事は、Future Tech Night #8というイベントでお話した内容を記事化したものです。
同イベントの他の発表も記事として投稿されてますので、ぜひご覧ください!

  1. Cypress入門~初心者でも簡単にE2Eテストが作れる~
  2. Cypress - 設定編(この記事です)
  3. 保守・拡張をしやすいカプセル化したCypress
  4. Cypress - 書きやすいテストの秘密と独自コマンドの実装