notebook

都内でWEB系エンジニアやってます。

AngularプロジェクトにCypressを入れてみる

今回はCypressをAngularのプロジェクトに入れて実際にテストを書いてみます

実際にブラウザを起動して操作するところが見れたりみたいな部分は他の記事でも紹介されているので今回は割愛して主にpluginを入れてテストしてみる話をしたいと思います

Cypress

JavaScript End to End Testing Framework | Cypress.io

www.cypress.io

end-to-endのtesting toolということで、ブラウザ操作を自動化したりスクリーンショットを撮ったり、操作を動画として保存したりとE2Eテストでやりそうなことはだいたいカバーしているっていうイメージですね

AngularでCypress

  • Angularのバージョンは下記となります
$ npx ng version
Angular CLI: 7.3.8
Node: 10.15.3
OS: linux x64
Angular: 7.2.14

cypress + Angularのschematicsが公式で紹介されていたのでng addで対応できます

briebug/cypress-schematic: Add cypress to an Angular CLI project

github.com

READMEにしたがって追加してみます

$ npm install --save @briebug/cypress-schematic
$ npx ng add @briebug/cypress-schematic

? Would you like to remove Protractor from the project? Yes
DELETE e2e
CREATE cypress/tsconfig.json (114 bytes)
CREATE cypress/plugins/cy-ts-preprocessor.js (387 bytes)
CREATE cypress/plugins/index.js (155 bytes)
UPDATE package.json (2989 bytes)
UPDATE angular.json (4915 bytes)

? Would you like to remove Protractor from the project?

と質問されるので今回はYesで既存のprotractorのコードは削除します

すると下記操作が自動でされます

  • protractor用のe2eディレクトリの削除
  • angular.jsonからe2e設定の削除

そして下記3ファイルが追加されます

  • cypress/plugins/cy-ts-preprocessor.js
  • cypress/tsconfig.json
  • cypress/plugins/index.js

ちゃっかりtypescriptプラグインまで動くようにしてくれているようですね!

schematicsのおかげでng addコマンドを打つだけでいろいろ面倒みてくれるのは体験としてとても良いですね

実際に生成されたファイルは下記3つです

  • cypress/plugins/index.js
const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor')

module.exports = on => {
  on('file:preprocessor', cypressTypeScriptPreprocessor)
}

そのままではありますがfile:preprocessorでTypeScriptプラグインからrequireしてきたオブジェクトを指定してあげることで事前にTypeScriptからコンパイルさせることができるようですね

preprocessorに関してはドキュメントにもあるようにTypeScript以外にもいくつかあるようです

Plugins | Cypress Documentation docs.cypress.io

  • cypress/plugins/cy-ts-preprocessor.js
const wp = require('@cypress/webpack-preprocessor')

const webpackOptions = {
  resolve: {
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: [/node_modules/],
        use: [
          {
            loader: 'ts-loader'
          }
        ]
      }
    ]
  }
}

const options = {
  webpackOptions
}

module.exports = wp(options)

webpackのpreprocessorを使っているのでTypeScriptに合わせた設定を渡してあげるということですね

webpackは正直雰囲気でしかさわれないのでそういうことなんだと言うことで覚えておきます・・・

  • cypress/tsconfig.json
{
  "extends": "../tsconfig.json",
  "include": ["integration/*.ts", "support/*.ts", "../node_modules/cypress"]
}

cypressで使うテストコードようのtsconfigです

実行してみる

すでにjsで一部テストを書いていたのでそのままtsに変えてテストしてみます

  • cypress/integrations/demo-spec.ts
describe('viewchildren', () => {
  const urlBase = 'http://localhost:4200/';
  it('demo with typescript', () => {
    cy.visit(`${urlBase}viewchildren`);

    cy.get('i.fa-exclamation-circle').eq(0).click();
    cy.get('div.popover-content')
      .eq(0)
      .should('have.text', 'a');
  });
});

操作としてはこんな感じの操作をしています

f:id:swfz:20190513232724g:plain

$ npx cypress run -s cypress/integration/demo-spec.ts

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

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:    3.2.0                                                                              │
  │ Browser:    Electron 59 (headless)                                                             │
  │ Specs:      1 found (demo-spec.ts)                                                             │
  │ Searched:   cypress/integration/demo-spec.ts                                                   │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


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

  Running: demo-spec.ts...                                                                 (1 of 1)

  (Results)

  ┌────────────────────────────┐
  │ Tests:        1            │
  │ Passing:      1            │
  │ Failing:      0            │
  │ Pending:      0            │
  │ Skipped:      0            │
  │ Screenshots:  0            │
  │ Video:        true         │
  │ Duration:     8 seconds    │
  │ Spec Ran:     demo-spec.ts │
  └────────────────────────────┘


  (Video)

  - Started processing:   Compressing to 32 CRF
  - Finished processing:  /home/vagrant/sandbox/ngx-sample/cypress/videos/demo-spec.ts.mp4 (7 seconds)


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

  (Run Finished)


      Spec                                                Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔ demo-spec.ts                              00:08        1        1        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    All specs passed!                           00:08        1        1        -        -        -

成功しました

ちゃんとtsでも実行できる事を確認

楽ちんですね

cypressでxpathを使って要素を取得したい

個人的にはDOMの操作とかはxpathで指定してとってきたいと思っている派なのでCypressでもxpathが使いたいです

ということでpluginが用意されているので使ってみます

cypress-io/cypress-xpath: Adds XPath command to Cypress test runner

github.com

  • cypress/support/index.js
require('cypress-xpath')

を追加するだけでOKなようです

早速やってみます

と思ったのですが Can't use "cy.xpath()" with "Typescript" plugin #28 のissueにある通りtypescriptプラグインを入れているとエラーになるようです

他プラグインとの併用

TypeScript化もさくっといけたしさぁ書くか!と思っていたのに水を差されてしまいました。

github.com

上記issueに書いてあるように型定義を入れろと言ってるようです

cypressで使用するTypeScriptのファイルはcypress/tsconfig.jsonで設定をしているのでその中のincludeに型定義ファイルがあるディレクトリを指定します

幸いcypress-xpathに関してはindex.d.tsが存在したのでディレクトリを指定するだけでOKそうです

  • cypress/tsconfig.json
include: [
  .....
  .....
  .....
  "../node_modules/cypress-xpath"
]

とすればエラーになることはなくなります

index.d.tsがないライブラリに関しては適当なディレクトリを作って勝手に定義してしまっても大丈夫には大丈夫です

完全ワークアラウンドですが本家が対応するまではこうするしかないかなといった感じですね

後述するcypress-visual-regressionはこの方法で対応しました

custom-typesというディレクトリを用意してその中にcypress-visual-regression用の型定義ファイルを用意しました

最終的なcypress/tsconfig.jsonと追加した型定義情報

  • cypress/tsconfig.json
{
  "extends": "../tsconfig.json",
  "include": ["integration/*.ts", "support/*.ts", "../node_modules/cypress", "../node_modules/cypress-xpath", "custom-types"]
}
  • cypress/custom-types/cypress-visual-regression.index.d.ts
// for cypress-visual-regression
declare namespace Cypress {
  interface Chainable<Subject> {
    compareSnapshot(name: string, threashold?: number): void;
  }
}

他には、Cypress.Commands.addで独自コマンドを入れたりしている場合はその都度型定義ファイルを追加しないと怒られるのでカジュアルにCypress.Commands.addなどをやっている場合は対応が必要そうです

正直面倒だなと思ってしまいますね。。。

何はともあれ実際に書いてみます

  • cypress/integration/demo-spec.ts

1つitを足して同じような処理をxpathで取得して書いて見ました

describe('viewchildren', () => {
  const urlBase = 'http://localhost:4200/';
  it('demo with typescript', () => {
    cy.visit(`${urlBase}viewchildren`);

    cy.get('i.fa-exclamation-circle').eq(0).click();
    cy.get('div.popover-content')
      .eq(0)
      .should('have.text', 'a');
  });

  it('demo with xpath', () => {
    cy.visit(`${urlBase}viewchildren`);

    cy.xpath('//i[contains(@class, "fa-exclamation-circle")]').eq(1).click();
    cy.xpath('//div[contains(@class, "popover-content")]')
      .should('have.text', 'b');
  });
});
$ npx cypress run -s cypress/integration/demo-spec.ts
====================================================================================================

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:    3.2.0                                                                              │
  │ Browser:    Electron 59 (headless)                                                             │
  │ Specs:      1 found (demo-spec.ts)                                                             │
  │ Searched:   cypress/integration/demo-spec.ts                                                   │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


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

  Running: demo-spec.ts...                                                                 (1 of 1)

  (Results)

  ┌────────────────────────────┐
  │ Tests:        2            │
  │ Passing:      2            │
  │ Failing:      0            │
  │ Pending:      0            │
  │ Skipped:      0            │
  │ Screenshots:  0            │
  │ Video:        true         │
  │ Duration:     18 seconds   │
  │ Spec Ran:     demo-spec.ts │
  └────────────────────────────┘


  (Video)

  - Started processing:   Compressing to 32 CRF
  - Compression progress:  74%
  - Finished processing:  /home/vagrant/sandbox/ngx-sample/cypress/videos/demo-spec.ts.mp4 (13 seconds)


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

  (Run Finished)


      Spec                                                Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔ demo-spec.ts                              00:18        2        2        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    All specs passed!                           00:18        2        2        -        -        -

cypress-visual-regression

cypress-visual-regression/command.js at master · mjhea0/cypress-visual-regression

github.com

visual regression test用のプラグインもあるようです(中身読んだらそんなに難しいことはしてなさそうだった)

READMEにしたがって使ってみます

  • インストール
npm install --save-dev cypress-visual-regression
  • cypress/plugin/index.js
const getCompareSnapshotsPlugin = require('cypress-visual-regression/dist/plugin');

module.exports = (on) => {
  getCompareSnapshotsPlugin(on);
};
  • cypress/support/command.js
const compareSnapshotCommand = require('cypress-visual-regression/dist/command');

compareSnapshotCommand();

テストのファイルではどこのスナップショットかを識別する名前とfailと判断する閾値を引数として渡します

引数のデフォルトはコード読んだ感じ0になってました

今まで使ってるテストファイルにスナップショットを取るだけのケースを追加します

  • cypress/integration/demo-spec.ts
  it('demo with screenshot', () => {
    cy.visit(`${urlBase}viewchildren`);

    cy.xpath('//button[contains(@class, "btn-primary")]').eq(0).click();

    cy.compareSnapshot('demo-screenshot', 0.1);
  });
  • まずはベースの状態のsnapshotをとります
npx cypress run --env type=base --config screenshotsFolder=cypress/snapshots/base

ここで渡しているディレクトリのパスはリグレッションのときに参照するので固定の模様

このような状態のスナップショットが生成されます

f:id:swfz:20190513232805p:plain

次にソースコードに変更を加えて再度実行してみます

今現在a,b,cとあるのでdを追加してから実行してみます

--type=actualで実行します

$ npx cypress run -s cypress/integration/demo-spec.ts --env type=actual
====================================================================================================

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:    3.2.0                                                                              │
  │ Browser:    Electron 59 (headless)                                                             │
  │ Specs:      1 found (demo-spec.ts)                                                             │
  │ Searched:   cypress/integration/demo-spec.ts                                                   │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


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

  Running: demo-spec.ts...                                                                 (1 of 1)

  (Results)

  ┌────────────────────────────┐
  │ Tests:        3            │
  │ Passing:      3            │
  │ Failing:      0            │
  │ Pending:      0            │
  │ Skipped:      0            │
  │ Screenshots:  1            │
  │ Video:        true         │
  │ Duration:     35 seconds   │
  │ Spec Ran:     demo-spec.ts │
  └────────────────────────────┘


  (Screenshots)

  - /home/vagrant/sandbox/ngx-sample/cypress/snapshots/actual/demo-spec.ts/demo-screenshot-actual.png (1000x660)


  (Video)

  - Started processing:   Compressing to 32 CRF
  - Compression progress:  38%
  - Compression progress:  78%
  - Finished processing:  /home/vagrant/sandbox/ngx-sample/cypress/videos/demo-spec.ts.mp4 (25 seconds)


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

  (Run Finished)


      Spec                                                Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔ demo-spec.ts                              00:35        3        3        -        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    All specs passed!                           00:35        3        3        -        -        -

テスト結果としては成功してます

実行するとsnapshotsディレクトリにそれぞれスナップショットが生成されます

$ tree sypress/snapshots/

cypress/snapshots/
|-- actual
|   `-- demo-spec.ts
|       `-- demo-screenshot-actual.png
|-- base
|   `-- demo-spec.ts
|       `-- demo-screenshot-form-base.png
`-- diff
    `-- demo-spec.ts
        `-- demo-screenshot-diff.png
  • 変更前 -> base
  • 変更後 -> actual
  • 差分 -> diff

という感じにスクリーンショットが生成されます

  • base f:id:swfz:20190513232821p:plain

  • actual f:id:swfz:20190513232832p:plain

  • diff f:id:swfz:20190513232847p:plain

ちゃんと差分が検出されていますね

このくらいの差分だと0.1の閾値でもfailしないようです

0にしてfailさせると下記のような出力になりました

npx cypress run -s cypress/integration/demo-spec.ts --env type=actual

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

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:    3.2.0                                                                              │
  │ Browser:    Electron 59 (headless)                                                             │
  │ Specs:      1 found (demo-spec.ts)                                                             │
  │ Searched:   cypress/integration/demo-spec.ts                                                   │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


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

  Running: demo-spec.ts...                                                                 (1 of 1)

  (Results)

  ┌────────────────────────────┐
  │ Tests:        3            │
  │ Passing:      2            │
  │ Failing:      1            │
  │ Pending:      0            │
  │ Skipped:      0            │
  │ Screenshots:  2            │
  │ Video:        true         │
  │ Duration:     17 seconds   │
  │ Spec Ran:     demo-spec.ts │
  └────────────────────────────┘


  (Screenshots)

  - /home/vagrant/sandbox/ngx-sample/cypress/snapshots/actual/demo-spec.ts/demo-screenshot-actual.png (1000x660)
  - /home/vagrant/sandbox/ngx-sample/cypress/snapshots/actual/demo-spec.ts/viewchildren -- demo with screenshot (failed).png (1280x720)


  (Video)

  - Started processing:   Compressing to 32 CRF
  - Compression progress:  100%
  - Finished processing:  /home/vagrant/sandbox/ngx-sample/cypress/videos/demo-spec.ts.mp4 (10 seconds)


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

  (Run Finished)


      Spec                                                Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✖ demo-spec.ts                              00:17        3        2        1        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    1 of 1 failed (100%)                        00:17        3        2        1        -        -

ちゃんと差分が出てて失敗していることがわかります

これをCircleCIのartifactと組み合わせればビジュアルリグレッションテスト的な事ができそうです

まとめ

結局プラグイン入れただけになってしまいましたがこれで少しづつでもテストコードを書いていける状態が整いました

発端としては

  • Renovateを使って定期的にnpmパッケージのバージョンアップをさせたい
  • テスト書く必要がある
  • Bootstrapだったりデザイン系の変更も検知したい
  • ビジュアルリグレッションテスト的なことまでやってみたい

ということでCypressが良さそう!?というのを見つけたので試してみました

良かった点

  • 導入までとても簡単
  • ビデオまで撮ってくれる
  • screenshotがコマンド一発でOK
  • 実際の操作時のDOMの状態を保存しているのでデバッグがしやすい
    • どこでどういう失敗をしているのかわかりやすい

微妙な点

  • jQueryのセレクターかー。。。
    • 個人的にはスクレイピング系のDomの指定はxpathでやりたい派
  • plugin少ない
    • 開発もそこまで活発ではなさそう

次回はこのサンプルプロジェクトでCircleCIを使ってCIを自動化した話かCypressで実際にテストコード書いてったときの詳細の話をできればと思います