生産性向上ブログ

継続的な生産性向上を目指すエンジニアのためのブログ

Selenium ユーザー視点で Cypress を試したらめちゃくちゃ便利そうでした

この記事は、Selenium/Appium Advent Calendar 2017 の 23 日目です。

この記事では、ブラウザテストツールの Cypress の紹介を Selenium ユーザーである自分の視点から書きます。

Cypress とは

www.cypress.io

Cypress は、テストのセットアップ、作成、実行、デバッグなどをシンプルにするブラウザテストツールです。E2E テストを既存の Selenium のようなツールで実装・運用するときにありがちな辛い体験を改善して、開発者を幸せにすることが目的のようです。

Cypress は、3 年以上の間 private beta だったのですが、今年の 10 月に public beta になり、そのタイミングで大半が OSS となりました。

www.cypress.io

Cypress は、ThoughtWorks 社の Technology Radar の今年 11 月に更新されたバージョンの Tools 部門で ASSESS(調査する価値がある)として位置づけられています。

Selenium との違い

Cypress は Selenium と比較されることが多くなると思われるので違いを文章で簡単にまとめてみますが、実際に体験したほうが違いを実感しやすいと思うのでここは読み飛ばしてもらっても大丈夫です。

まず根本的な違いとして、Selenium はあくまでブラウザを自動操作するツールであってテスト以外の用途(スクレイピングや手元の操作の自動化など)でも使えるのに対して、Cypress は完全にテスト目的に特化しています。なので、要素の表示を待ったりページ遷移の完了を待ったりといった不安定なテスト対策は標準で行われます。現状では E2E テストや統合テストのための機能が多いですが、JavaScript のユニットテストのサポートも強化していくようです。

次にアーキテクチャ面ですが、Selenium は WebDriver API 経由でネットワーク越しにブラウザを操作するのに対して、Cypress はブラウザ上でランナーが動き、そのランナーの中の iframe でテスト対象のページを開きます。これにより、テスト対象のページで発生する JS イベントをテストコード側で listen したりできます。また、ブラウザ操作の速度も速く、可能なかぎり速くテストを実行していくことができます。

また、Cypress はブラウザの裏で Node.js のサーバープロセスが動いており、これがブラウザ上のテストランナーとやりとりすることによってブラウザ外の操作(スクリーンショットやビデオ、ファイルシステム、ネットワークのコントロールなど)を可能にしています。これにより、サーバーからのレスポンスをモックしてテストを行いやすくしたり、テスト失敗時にデバッグしやすさを向上させたりできます。

一方で、Selenium と比較するといくつか制限も存在します。

まず、ブラウザ上でテストコードが実行されるというアーキテクチャ上、JS 以外の言語は使えません。サーバーサイド側の機能を直接使うこともできないですが、このあたりはリクエストを投げたりローカルでコマンドを実行する機能などが用意されてはいます。

また、iframe 上でテスト対象のページが読み込まれるという構造なので、複数タブを扱うことができません。このため、target="_blank" の要素をクリックするときは事前に target 属性を削除するなどの工夫が必要になってしまいます。

同様に、複数のブラウザウィンドウを同時に操作ということもできないので、チャットのテストのように 2 つ以上で同時に操作するテストケースには対応できません。

そして、Same-origin の制限もあります。1 つのテストの中で複数のドメインをまたぐようなテストはできないとのことです(同じドメイン下の複数サブドメインをまたぐことは可能)。

他にも、Chrome と Electron にしか対応していない、並列実行の機能が足りていないといった部分もありますが、Roadmap を見るとこのあたりは将来的に対応する予定があるようです。

試してみる

ここまで長々と説明してきましたが、実際に動かしてみるのが一番わかりやすいと思うので動かし方を書いていきます。基本的には、公式ドキュメントの Getting Started あたりの内容を試していきます。

今回試した内容は、↓のリポジトリで公開しています。

github.com

まず、Cypress には Test Runner と Dashboard Service の 2 つがあります。Test Runner はテストをローカルで実行する部分で、完全無料かつ OSS です。Dashboard Service はテスト結果を記録するための Web サービスで、テスト結果を Public(誰でも見られる)にするなら無料、Private(招待したユーザーのみ見られる)だと有料(現在はベータ期間なので無料)になります。

Test Runner

インストール

まず、適当な npm プロジェクトを作成し、以下のコマンドで Cypress をインストールします。

$ npm install cypress --save-dev

Cypress はオールインワンなので、テストランナーや assertion ライブラリを別にインストールする必要はなく、cypress パッケージだけインストールすればもう使えます。このあたりは Selenium と比較すると圧倒的に楽です。

起動

次は、cypress を起動してみます。

$ ./node_modules/.bin/cypress open

すると、初回起動時にいくつかのファイルやディレクトリが作成され、以下のような GUI が表示されます。

f:id:miya-jan:20171223124514p:plain

テストを書いてみる

example_spec.js を実行してみてもいいですが、まずは自分でテストコードも書いてみます。作成された cypress/integration ディレクトリの下に sample_spec.js を作成して以下のようなコードを書きます。

describe('My First Test', function() {
  it('Visits the Kitchen Sink', function() {
    cy.visit('https://example.cypress.io')
  })
})

すると、先ほど起動した Cypress のウィンドウの Tests タブ上に sample_spec.js が表示されるのでそれを押すと、ブラウザが立ち上がってテストが実行されます。

f:id:miya-jan:20171223125214p:plain

cy.visit() は指定した URL を開くコマンドで、デフォルトだと 200 番台と 300 番台以外のステータスコードが返ってきたときはエラーになります。また、テスト中に JavaScript エラーが発生したときはテスト失敗になるようです。このあたりは Selenium だと実現が厳しい部分だったので、標準で対応されているのはとても嬉しいです。もちろん、これらは設定でオフにもできるようです。

次は要素を取得してみるためにコードを次のように修正します。

describe('My First Test', function() {
  it('finds the content "type"', function() {
    cy.visit('https://example.cypress.io')

    cy.contains('type')
  })
})

すると、ファイルが保存された時点で先ほど開いたブラウザでテストが自動で実行されました。どうやら watch 的な機能も標準で対応してくれているようです。

上のコードは、cy.contains() で 'type' という text を持つ要素を取得しています。

ちなみに、'type' の代わりに 'hype' とか存在しない text を書くと cy.contains() で 4 秒ほど待ってからエラーになります。これは、Cypress が非同期に表示される要素の対策のために、 4 秒間の間、要素が表示されるのを待って確認するという処理を繰り返しているからです。Selenium における WebDriverWait 相当のことを自動でやってくれているわけですね。

要素に対する操作はメソッドチェーンで行うことができます。以下のコードでは、取得した要素をクリックしています。

describe('My First Test', function() {
  it('clicks the link "type"', function() {
    cy.visit('https://example.cypress.io')

    cy.contains('type').click()
  })
})

assert を行うには should() を使います。以下のコードでは URL の確認と要素に入力できることを assert しています。

it("Gets, types and asserts", function() {
  cy.visit('https://example.cypress.io')

  cy.contains('type').click()

  // Should be on a new URL which includes '/commands/actions'
  cy.url().should('include', '/commands/actions')

  // Get an input, type into it and verify that the value has been updated
  cy.get('.action-email')
    .type('fake@email.com')
    .should('have.value', 'fake@email.com')
})

cy.get() で CSS セレクタ記法で要素を取得できます。

デバッグしてみる

ブラウザのコマンドログ上でコマンドをクリックすると、そのコマンドが実行された時点の DOM スナップショットを見ることができます。

f:id:miya-jan:20171223131450p:plain

CLICK を選択すると上の画像のようにクリックイベントが発生した場所がピンポイントで赤く表示されます。さらに、before と after というメニューパネルが表示され、イベント発生前後両方の DOM スナップショットを切り替えて見ることができます。めちゃくちゃ便利です。

そして、なんとこの状態で Chrome の Developer Tools を開くことができます。

f:id:miya-jan:20171223131904p:plain

Selenium だとテスト再実行してステップ実行してみたいなめんどくさいデバッグが必要だったのが、テスト再実行すら必要なくデバッグすることができるようになっています。これは、再現性の低いテスト失敗を調査するときにめちゃくちゃありがたみを実感できるだろうと思います。

さらに、以下のようなイベントも自動でコマンドログに記録されます。

  • XHR リクエスト
  • URL の hash 変更
  • ページロード
  • form の submit

さらにさらに、Developer Tools のコンソールにデバッグ情報が出力されます。例えば、GET コマンドをクリックすると、コンソール上に取得した要素の DOM 情報などが表示されているのを確認できます。

f:id:miya-jan:20171223132315p:plain

ステップ実行のための機能も用意されており、以下のコードのように cy.pause() でステップ実行が可能です。

it("clicking 'type' shows the right headings", function() {
  cy.visit('https://example.cypress.io')

  cy.pause()

  cy.contains('type').click()

  // Should be on a new URL which includes '/commands/actions'
  cy.url().should('include', '/commands/actions')

  // Get an input, type into it and verify that the value has been updated
  cy.get('.action-email')
    .type('fake@email.com')
    .should('have.value', 'fake@email.com')
})

f:id:miya-jan:20171223132632p:plain

このあたりのデバッグ機能の充実ぶりは、テスト用途に特化して作られている Cypress の大きな強みですね。

Headless

ここまでは GUI 上でブラウザを立ち上げてテストしてきましたが、以下のように cypress run コマンドで headless モードでも実行できます。

$ ./node_modules/.bin/cypress run

主に CI などの用途で使います。

デフォルトでは Electron で実行されますが、オプションでブラウザを指定できます。

$ ./node_modules/.bin/cypress run --browser chrome

テスト失敗時や cy.screenshot() で撮影された画像が cypress/screenshots ディレクトリに、テスト実行時の動画が cypress/videos に保存されます。

CircleCI 2.0 で CI してみる

Cypress は大抵の CI ツールやサービスで動かすことができるようになっていますが、今回は CircleCI 2.0 で CI してみます。

CircleCI にテスト結果を認識してもらうために mocha の reporter 関連をインストールします。

$ npm install mocha --save-dev
$ npm install mocha-junit-reporter --save-dev
$ npm install mocha-multi-reporters --save-dev

そして、config.json を作成して以下のように記述します。これでテスト結果が JUnit 形式で results/results.xml に出力されます。

{
  "reporterEnabled": "spec, mocha-junit-reporter",
  "mochaJunitReporterReporterOptions": {
    "mochaFile": "results/results.xml"
  }
}

次に、package.json の scripts に以下を記述します。

"scripts": {
  "ci": "cypress run --reporter mocha-multi-reporters --reporter-options configFile=config.json"
}

CircleCI の設定として、.circleci/config.yml を以下のように書きます。Cypress には公式の Docker イメージがあるので、それを使います。

version: 2

jobs:
  build:
    docker:
      - image: cypress/base:6
        environment:
          TERM: xterm
    working_directory: ~/app
    parallelism: 1
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-npm-deps-{{ checksum "package.json" }}
            - v1-npm-deps
      - run: npm install
      - save_cache:
          key: v1-npm-deps-{{ checksum "package.json" }}
          paths:
            - node_modules
      - run:
          name: Running E2E tests
          command: npm run ci
      - store_test_results:
          path: results
      - store_artifacts:
          path: cypress/videos
      - store_artifacts:
          path: cypress/screenshots

これらをすべてコミットして CircleCI 側でリポジトリを有効にすると無事動き、テスト結果が認識されました。

f:id:miya-jan:20171223135131p:plain

Artifacts にスクリーンショットや動画も保存されています。

f:id:miya-jan:20171223135235p:plain

Dashboard Service

Dashboard Service は、主に CI で実行したテストの記録にアクセスできるようにするサービスです。

以下の記録を確認できます。

  • 失敗、スキップ、成功したテストの数
  • 失敗したテストのスタックトレース
  • スクリーンショット
  • 動画

また、誰がテスト記録にアクセスできるか管理するための機能も存在します。

Setup

cypress open で開いたウィンドウの Runs タブから "Setup Project to Recordをクリックしてプロジェクト名などの設定を行います。すると、cypress.json``` にプロジェクト ID が設定されるので、それをコミットします。

ウィンドウ上にレコードキーが表示されるので、それをパラメータか環境変数に指定して cypress run コマンドを実行するとテストがダッシュボードサービスに記録されます。

$ cypress run --record --key <record key>
$ export CYPRESS_RECORD_KEY=<record key>
$ cypress run --record

以下のようにテスト結果を確認できます。

f:id:miya-jan:20171223140358p:plain

動画やスクリーンショットもブラウザ上から確認できます。

f:id:miya-jan:20171223140726p:plain

f:id:miya-jan:20171223140750p:plain

今回のサンプルは誰でもダッシュボードを見られるようになっているはずなので、↓からアクセスしてみてください。

https://dashboard.cypress.io/#/projects/nmpfc6/runs

CI からダッシュボードにテスト結果を送る

先ほどの設定を少し修正すれば CI 実行時にダッシュボードにテスト結果を送れます。

まず、レコードキーを CircleCI のプロジェクトの Environment Variables に CYPRESS_RECORD_KEY というキーで保存します。

そして、package.json の scripts に以下のように --record を追記するだけです。

"scripts": {
  "ci": "cypress run --record --reporter mocha-multi-reporters --reporter-options configFile=config.json"
}

所感

Cypress は、ブラウザテストの開発体験を楽しいものにしてくれるオールインワンのフレームワークだと感じました。特に、ブラウザテストはデバッグまわりが辛かったのを大幅に改善してくれそうです。また、地味にテストの実行速度がサクサクなのが嬉しいです。今回は詳細を紹介できなかったですが、ネットワークや日時まわりをモックできるところもこれまでのブラウザテストとは違う体験を提供してくれそうに思います。

まだベータではありますが、個人的には、あまり規模が大きくなくてマルチブラウザの必要性もないテストは Cypress を実戦投入してみたいです。今後の機能追加次第では大規模な用途にも向いてくるのではないかと思います。

もちろん制限などもあるので Selenium などの既存ツールの完全な置き換えにはならないとは思います。しかし、1 つのツールですべてをテストしないといけないというわけではないので、選択肢の一つとして Cypress を検討してみるのはありだと思います。

Cypress は OSS でコントリビュートもできるので、ブラウザテストをより楽しい体験にしていきたい開発者や QA の人はぜひぜひ試していっていただきたいと思います。