Cypressまとめてみた~概要&推しポイント・導入方法・デメリットまで! 特盛版~

はじめに

こんにちは!
ecbeing3年目、R&D部門所属のいかちゃんです。

前回や前々回には、Dockerの記事やバーコードリーダーに関する記事を書きました。
blog.ecbeing.tech blog.ecbeing.tech

そして今回は…テスト自動化系ツールの紹介として、E2EテストツールのCypressについてまとめてみようかと!

テスト自動化…実に素晴らしい響きですよね。
R&D部門では定期的にリリースを行うSaaS系サービスが多いことから、テスト自動化の熱は非常に高かったのですが…。
機能開発やインフラ整備等々でなかなか導入できなかったのが現状でした。

そんな中、何とかローンチしたての小規模プロダクトにてCypressを使用したテスト自動化に成功しましたので。
Cypressとは何か&推しポイント紹介、そしてその導入方法、さらにはCypressを使っていった上で感じたデメリットを見ていこうかと。
(色々要素を詰め込んだ分、特盛ボリュームの記事となっております)

最後にはテスト自動化を行った所感も載せています。
プロジェクトへテスト自動化を導入するにあたり参考になれば幸いです…。

それではいきましょう!

Cypressとは

www.cypress.io

Cypressは「E2Eテストフレームワーク」の1種です。
似たような物としてはSeleniumが挙げられますね。

余談: E2Eテストとは

E2Eテストは「End to Endテスト」の略となっています。
「User Interface Test」とも呼ばれるそうです。

その名が示す通り「プロジェクトの完成形に近い物に対して」「エンドユーザーと同じようにブラウザを操作し、期待通りの処理となっているのかを検査する」物です。

単体テストに比べると、実際にサーバーを立ち上げたりネットワーク通信の影響をダイレクトに受けたりするため、実行に非常に時間がかかる&ネットワーク状況によっては不安定になることも多いです。
しかし、実際に稼働している状況下でテストできるという大きなメリットもあります。

以前は「private beta」としてリリースされていましたが、2017/10/09に「public beta」版としてv1.0.0がリリースされました。

2021/03/11現在の最新バージョンは2021/02/18にリリースされたv6.6.0
v1.0.0が2017/10/09に出た事を考えると、結構頻繁にアップデートされているようです。

GithubのStar数は28.2k、Watch数は498と結構HOTなフレームワークなんじゃないかなと。
※2021/03/11時点

特徴(推しポイント)としては下記が挙げられます:

  • JavaScriptでテストコードが書ける
  • マルチブラウザ対応(Chrome, Firefoxなど)
  • 導入が簡単

特にJavaScriptでテストコードが書けるのは個人的推しポイントです。
Cypressのテストコード記述方法自体もとても簡単なので、JavaScriptをかじったことがある方ならサクサク書けるかと。

おまけにマルチブラウザ対応、更に同一のコードで異なるブラウザに対するテストも書くことが出来ます…!
ブラウザによってテストコードを書き分ける手間からも開放されますね…。

極めつけは「導入が簡単」ということ。
こちらについては実際にCypressの導入方法を見ていきながら、どれほど簡単に導入が出来るのかを見ていただければと思います。

それではいきましょう!

Cypressを導入してみよう

Cypressはnpmモジュールであるため、はじめにnpmモジュールをインストールする環境…つまり「Node.jsプロジェクトの作成から」行います。
※もちろん、既存のNode.jsプロジェクトがあるなら省略可です

# フォルダを作成、移動
$ mkdir cypress-test
$ cd cypress-test
# Node.jsプロジェクト作成
$ npm init

ここまで来たら、いよいよCypressを導入していきます。

# Cypressをインストール
$ npm install cypress

その後、package.jsonに下記のCypress起動スクリプトを追加し:

~中略~
  "scripts": {
    "cypress:open": "cypress open"
  },
~中略~

追加したスクリプトを実行すると…。

$ npm run cypress:open

下記の様な画面が開くと思います。

Cypress起動画面

…これで導入は完了です!
たったの4,5コマンドで環境が整ってしまうというお手軽さです…! いやぁ素晴らしい。

また、この時点ですでにマルチブラウザテストも出来ます
先ほどの画面のうち、以下のプルダウンから対象のブラウザが選べちゃいます。

Cypressブラウザ選択画面

ただ、現時点では下記のブラウザしか対応していないようですのでご注意を。

  • chromium系ブラウザ
    • Chrome
    • Chromium版(最新Ver)Edge
    • Electron
  • Firefox

なお、先ほどは省略しましたが…Cypress初回起動時には下記の様なモーダルが開くと思います:

Cypress初回起動時に出てくるモーダル画面

こちらは「Cypressのサンプルとしておたくのフォルダ内にCypressのサンプルコードを置いといたよ」という意味です。
この画面が開いた時点でフォルダを確認すると、cypressというフォルダ、そしてcypress.jsonというファイルが追加されているかと。

Cypressの導入がいかに楽なのか、実感頂けたかと思います…。
折角Cypressの実行環境も整いましたので、ついでにテストコードの書き方も見てみましょう!

テストコードを書いてみる

まずはテストコードの書式から。
Cypressは下記の様な感じでテストコードを記述することが出来ます。

describe('[テスト内容タイトル]', () => {
  it('[テスト内容サブタイトル1]', () => {
    // 任意処理
  });
  it('[テスト内容サブタイトル2]', () => {
    // 任意処理
  });
});

itは「各テストケース単位」、describeは「複数のテストケースをまとめた物」として捉えていただけると分かりやすいかなと。
上記のコードをCypress上で実行すると、下記の様に出力される事からも何となく察していただけるかと:

実行時のCypress画面

そして、各テストケースの中身…任意処理部分には様々な内容を書くことが出来ます。
例えば:

  • ページ遷移
  • セレクター指定でDOM要素を指定し、その値を検査
  • テキストボックスへの入力&値の検査
  • 任意のDOM要素をクリック
  • スクリーンショットの取得

…などなど。
ユーザーがブラウザ上で実際に操作する動きは大体表現できるかなと(よほど込み入ったことでない限り)。

さて、実際のテストコードの例を見てみましょう。
例えば「Googleにアクセスして」「検索ボックスにて検索し」「検索結果のスクリーンショットを取る」ソースコードは下記となります:

describe('Googleのテスト', () => {
  it('Googleの検索欄をスクショ', () => {
    // Googleにアクセス
    cy.visit('https://google.com');
    // 検索欄に値を入力
    cy.get('input[name="q"]') // 検索欄のDOM取得
      .type('ecbeing labs') // 検索欄に値を入力
      .type('{enter}'); // CypressにてEnterキーを疑似的に押下
    // スクリーンショットを取る
    cy.screenshot('searchResult');
  });
});

試しに上記を実行してみましょう。
先ほど生成されたcypressフォルダ内の、integrationというフォルダにtest.spec.jsという名称で保存します。

すると、先ほど開いていたCypressの画面にtest.spec.jsと書かれたリンクが出てくるので…。

テストコード配置後のCypress画面

後はリンクを押せば、実行完了です。

ちなみに、保存されたスクリーンショットはcypress/screenshots/test.spec.jsフォルダ内に生成されます。
たったの1コマンドでスクリーンショットが取れるお手軽さ…いいですよねぇ。

他にもCypressには「テストコードファイル間で共通の処理を記述出来たり」「別ファイルに定義した環境変数ファイルを参照出来たり」「Cypressの既存処理を上書きor様々なプラグインを導入出来たり」といろいろ出来ますが…。
これら全部を紹介してると、それだけで記事が1本出来上がりかねないので…ここでは省略します。
それでも、Cypressがどんな感じで使用できるかは大体分かって頂けたかなと!

ただ、悲しきかなどんなツールにもデメリットはあってしまうもの…実際私もCypressを使う上で「あちゃ~これが出来ないのか…」となり、頭を抱えたものです。
というわけで、お次はCypressのデメリットを見ていきましょう。

Cypressのデメリット

Cypressについて、フォーラムや公式ドキュメントにすら載ってるほどの広く知られているデメリットは下記の3つがあります:

  • 別タブを開いた動作確認が出来ない
  • SafariやIEでは使用不可
  • クロスドメインアクセスが不可能

そして…私個人としては、下記の事項もデメリットとして感じました:

  • 記述に慣れるまでに時間がかかる

1つ1つ順番に見ていきましょう。

別タブを開いた動作確認が出来ない

Cypressのデメリットの個人的2大巨頭の片割れがこちら…なんと、Cypressでは別タブを開いて動作確認が出来ません…。

具体的には「別タブでリンクを開き→その別タブ内の値を取得する」といったようなことが出来ません。
※そもそもテストコード中に別タブに移動することが出来ません

これはCypressの実行方式に問題があります。
参考: Cypress Docs

Cypressはテストコードを「JavaScriptで実行する」方式を採用しており…。
(JavaScriptでテストコードが書けるのもこのためです)
つまりJavaScriptで出来ないことは、Cypress上でも実現が不可能という事を指します。

単一のJavaScriptファイルで「別タブを開いて」「その別タブ内の値を取得する」…なんてことは不可能ですよね?つまりはそういうことです…。

ちなみに聡い方ならお気づきでしょう「その実行方式ならなぜ画面遷移が可能なのか」という理由は、Cypress経由でアクセスするサイトはiframe上で実行されるからです。
Cypressの実行中画面をChrome DevToolsで開くと、テスト対象のサイトはiframeの中にいる事が分かります:

Cypressがiframe内にてサイトを開いていることを示すChrome DevToolsスクショ画面

iframe内での画面遷移はJavaScript経由でも出来ちゃいますので「画面遷移は実行可能」というわけです。
(でもiframeで別タブは開けませんよね?そういうことです…)
※参考記事: Selenium ユーザー視点で Cypress を試したらめちゃくちゃ便利そうでした - 生産性向上ブログ

ちなみに「それでも別タブを開くことを検査したい」という状況の対応として、Cypress側にて「ブラウザがその動作を実行するようにコードが書かれているかを確かめればいい」と表明しています。
具体的には「下記の様なコードを組めばいいじゃない」と:

cy.get('a[href="/foo"]') // 検査したいDOM要素取得
  .should('have.attr', 'target', '_blank') // aタグに`target="_blank"`が付いているか確認する

中々力技な気もしますが、この状況下であれば上記のようにするしかないのかなと思います。

なお、JavaScriptで出来ないことは、Cypress上でも実現が不可能というデメリットは次の章にもつながってきます…。

SafariやIEでは使用不可

Cypressのデメリットの個人的2大巨頭のもう片割れがこちらです。
2021/03/11現在、Cypressは「Chromium系ブラウザ」と「Firefox」にしか対応していません

といのも、どうもCypressは内部でPuppeteerを使用しているらしく。
参考ソース

Puppeteerのクロスブラウザ対応を見てみると、Cypressの対応ブラウザと一致することから…。
Cypressの対応内容はPuppeteerの対応状況に依存しているのかもです。

Puppeteer公式Docsを見てみると、Puppeteer側もSafariなどのクロスブラウザ対応には意欲的なようなので…。
PuppeteerがSafariや他ブラウザに対応されたその時には、Cypressも対応が行われるかもしれません。

Cypressのクロスブラウザ対応の最新状況が知りたい場合は、下記のIssueが参考になると思いますので参考に:

異なるオリジン間へのアクセスが不可能

Cypressでは「テストコード内で異なるオリジン間へのアクセスが不可能」となっています。
※参考: 公式Docs

実際に下記の様な「異なるオリジン間へアクセスを行う」テストコードを作成すると:

describe('異なるオリジン間のアクセス', () => {
  it('異なるサイトへアクセス', () => {
    cy.visit('https://google.com');
    cy.visit('https://apple.com');
  });
});

以下の様なエラー文が出ます:

Cypressエラー画面

対策としては、cypress.jsonに以下の様な記述を行えば動くようになります。

{
  "chromeWebSecurity": false
}

記述に慣れるまでに時間がかかる

こちらは私個人が感じたデメリットですが…。
Cypress…もっと言うと「JavaScriptを使用してサイトを動かす」という経験が初めてということもあり。
自分が想定したようにサイトを動かせる様になるまで、結構時間がかかってしまいました。
(前章で述べたPuppeteerもやったことなかったですしね)

Cypressには以前のSelenium IDEの様な「ブラウザ上の操作を記録してくれるツール」がないため、ブラウザ上の操作を全てJavaScriptで書くことになります。
(どちらかというとブラウザ操作記録機能があったSelenium IDEが凄いって話ですけどね)

例えば、要素を取得してクリックするだけなら下記の様なコードで済むのですが:

cy.get('#hoge').click()

「要素をドラッグアンドドロップして入れ替える」「特定のクラスを持つ子要素全てのテキストボックスに値を入力する」といった込み入ったことをしようとすると非常に面倒になってしまいます。

私の様な「Puppeteerを使用したことがない」×「JavaScriptを使用してサイトを動かしたことがない」…といった状況では、コードを書く時間の倍くらい学習時間が必要でした…。
もし「Puppeteerを使用したことがある」だったり「JavaScriptでスクレイピング等を通し、サイトを動かしてみたことがある」といった方であればそこまで学習時間は必要ないかもです。

更に、Cypress関連で参考になるテストコード集といった記事が少なく…。
先の例に出したような、ちょっと込み入ったことをやりたくなった際に参考にできるサンプルコードがなくてですね。
大体の場合「WikiかStackOverflowを見て実装のヒントを経て、実装する」という流れになってしまいます。
(私のググり方が悪かったのかもですが)

自分で創意工夫しコードを作っていく楽しさはもちろんあるのですが、その分時間がかかってしまいました。

ただ、これらを経て知識が十分についた状態なら、コードを書くのは早くなるかと。
私の経験談とはなりますが、全くの無知状態で1画面のテストコードを実装した時は12時間ほどかかりましたが…。
処理の共通化を積極的にしていたことや、自身のCypress力が上がったからか、その後別の画面を実装した際は3時間程度で実装できました。

幸いにも当時のプロジェクト状況に余裕があったこともあり、テスト自動化プロジェクトに集中できる期間があったので問題はなかったのですが…。
これがもし「時間のない中で突貫でやらなければならない」のだと、非常に苦しい思いをしただろうなと思います。
筆者の様な慣れていない方にCypressを使用したテスト自動化プロジェクトを任せる際は、十分に時間を取ってあげてから任せてあげたほうがいいかと。

デメリット総括

ここまでCypressの4つのデメリットを見てきました。

Cypressはその実装方針や使用ツールにより幾つかの制限がありますが…。
それをもってしても「JavaScriptで手軽にテストコードが書ける」「同じコードでクロスブラウザ対応可能」というメリットは大きいかと。

それにデメリットの内「別タブを開いた動作確認が出来ない」というのは、SPA形式のWebサービスでは問題にならないかなと思います。
※SPAでは別タブなんて開きませんしね

また、「SafariやIEでは使用不可」というデメリットも…。
Cypressを使用する用途を「新規開発や保守開発した際に、デグレーション(変更が既存の機能に影響すること)していないかを確認するため」と限定すれば回避できます。
この使用用途であればChrome等のどれか1ブラウザでテストが出来れば十分ですしね。

もしくは導入やテストコードの記述の楽さを取って「E2Eテストツールに慣れるために使用してみる」というのもいいと思います。
「テスト完了までの実行時間の遅さ」や「ネットワーク状況に左右されるテスト成否の不安定さ」など、E2Eテストならではの大変さはCypressを使用してももちろん経験可能なので…。 導入も記述も楽ですので、「E2Eテストってどんなもんなんだろ」という事を体感するにはいい機会を作れる…んじゃないかな~と思います!

今回挙げた他のデメリットも、上記のように使用用途や状況によっては考慮しなくて良くなることもあると思います。
Cypressはあくまでツールの1種なので、デメリットを考慮したうえで自分たちのプロジェクトにどう生かすかを検討してみるのも面白いのではないかと。
(他の技術選択でもそうですけどね)

もしcypessの他のデメリット要素が気になる方は、こちらの公式Docsが参考になると思いますので参照に:
docs.cypress.io

まとめ&テスト自動化の所感

本記事ではCypressの概要とその使用感、そしてCypressのデメリットまでを見ていきました。
(ちょっと盛りすぎなぐらいの記事ボリュームでしたね…)

Cypressはデメリットの章にも載せたような向き不向きはありますが、導入が非常にしやすい&JavaScriptでテストコードを書ける&他のブラウザで同じテストコードを使いまわせるなど様々なメリットがあります。
デメリット総括の章にて触れたように、条件さえ合えばとてもいいフレームワークだと思うので…。
テストコードをこれからやっていこう!という方で、条件が合えば是非是非試してみてください。

ここからはテスト自動化をやってみた所感をば。

今回Cypressを使用しテスト自動化やってみましたが…はっきり言って滅茶苦茶大変でした

先の章に上げたデメリットの内「記述に慣れるまでに時間がかかる」という点が非常に厄介でして。
章内でも触れた「サンプルコード例の少なさ」から「こういう風なテストコードを書きたい!」と思ってググっても、結局はWikiかStackOverflowにたどり着き…。
そこで実装のヒントを経て実装する…という流れを何度も何度も繰り返しました。
(まぁこれもこれで楽しいんですけど)

タスクのアサイン状況の結果で、幸いにもテスト自動化をやっていた時はそれだけに注力できたのがせめてもの幸いでした。
この作業を他の機能開発を行いながらやるというのを考えるとぞっとします…。

そして、冒頭にて「小規模のプロダクトに導入した」とありましたが…。
実はテストコード本体はあまり書けておらず。プロジェクトの最も主要な画面に対して行うのがやっとでしたね…。

職場にてテスト自動化に詳しいベテラン先輩からあったアドバイス:

E2Eは時間がかかるので、通常これは実施したいというもののみに削ぎ落としてから実施します。
(カバーできないところは単体テストでフォロー)
なので、プロジェクトに重要な処理をピックアップして、それらに優先順位決めてやったほうがいいかなと。

…の通り、テスト自動化はちょっとずつやっていった方がいいのかなと思います。

また、そのあとのメンテナンスも考えなければなりません。
現時点では「テストの導入だけ終わった」段階なので、メンテナンスの大変さはまだ未経験ですが…。
テストコードを書き始める方が大変なのか、はたまたメンテナンスの方が大変なのか…出来れば前者であってほしいなと思ってます。

ですが、テスト自動化が出来たおかげでデグレーションに目を尖らせることなく開発できるのはとても素敵な事なんじゃないかと思います!

Cypressはデメリットの章で述べたような幾つかの制限はありますが、Cypressであれば導入も非常に簡単ですし、慣れれば簡単にテストコードを書いていけますので…。
これを機に是非、E2EテストツールCypressを使用したテスト自動化…いかがでしょうか?

…と、非常に切りよく収まりそうなのでこの辺で。
最後に余談として、Cypressの小ネタを1点挙げてみました。もしよければ見ていってください。
それでは!

ecbeingではテスト自動化に積極的なエンジニアを募集しています!!

careers.ecbeing.tech

余談: Cypressではasync-awaitが使えない

古めではないJavaScriptをやってる方ならなじみ深い、同期処理の糖衣構文async, await
しかしこれが、Cypressだとなんと使用不可能です。

これは公式ドキュメントもその様に表明しています:

Why can’t I use async / await?
If you’re a modern JS programmer you might hear “asynchronous” and think: why can’t I just use async/await instead of learning some proprietary API? Cypress’s APIs are built very differently from what you’re likely used to: but these design patterns are incredibly intentional.

参考: Introduction to Cypress | Cypress Documentation

この原因を少し詳しく見てみると…。
Cypressは意図的にasync, await…もっと言うとPromise構文を使用していないそうです。
これはPromise構文に「リトライ機能がないから」としています。

E2Eテストではネットワーク状況の不調等により、記述したテストコード内の処理が想定通りになってくれない…なんてことは良くあります。
例えば「○○のURLに遷移してね」と書いても、ネットワーク状況が悪く中々遷移しない…みたいな。

こういったことを回避するため、Cypressの処理は「処理が1度失敗しても、一定期間リトライし続ける」様になっています。
これによりテスト実行時の安定性を高めているとのこと。

また、リトライ機構を挟んでいるからか「Cypressのコードは同期的に実行」されます。
例えば下記の様なコードだと:

describe('Hoge', () => {
  it('Fuga', () => {
    cy.visit('http://localhost:8080');
    cy.get('#hoge').click();
  });
});

3行目にてもし「アクセスに失敗」しても、一定期間はリトライされますが…。
4行目の処理は恐らくhttp://localhost:8080内の要素であることから、リトライ中に処理が実行されても絶対に失敗してしまいます。

そのため、上記の処理を実行すると「3行目の処理が終わってから4行目の処理をする」…つまり同期的に処理されます。
(というか、テストコードで非同期的に実行される前提でコード組めるわけもないですからね…)

ちなみにここでPuppeteerのコードを見てみると:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();

…という感じで、基本的には処理はasync-await構文を使用しているのが分かるかと。
恐らくCypressも、cy.[コマンド名]と付くものは内部では上記のような処理に展開されて使用されるのかな~と…。