Cypress

Cypressは素晴らしいE2Eテストツールです。これを考慮する大きな理由は次のとおりです。

  • 分離インストールが可能です

  • TypeScriptの定義がそのままの状態で使えます

  • 優れたインタラクティブなGoogle Chromeのデバッグ環境を提供します。これは、UI開発者が手動で作業する方法と非常によく似ています

  • より強力なデバッグとテストの安定性を実現するコマンド実行セパレーションを持っています(詳細は後述)

  • より脆いテストでより意味のあるデバッグエクスペリエンスを提供するための暗黙のアサーションがあります(詳細は以下のヒントを参照してください)。

  • アプリケーションコードを変更することなく、バックエンドのXHRを簡単に模倣して観察する機能を提供します(以下のヒントで詳しく説明しています)。

インストール

このインストールプロセスで提供される手順は、あなたの組織のボイラープレートとして使用できる素敵なe2eフォルダを提供します。

同じ手順を私の youtubeチャンネルで動画で紹介しています。

e2eディレクトリを作成し、cypress、TypeScriptをインストールし、typescriptとcypressの設定ファイルを追加します。

mkdir e2e
cd e2e
npm init -y
npm install cypress typescript
npx tsc --init --types cypress --lib dom,es6
echo {} > cypress.json

cypressのために別のe2eフォルダを作成するのには、いくつかの理由があります:

  • 別のディレクトリやe2eを作成すると、package.jsonの依存関係を他のプロジェクトと簡単に分離することができます。これにより依存性の競合が少なくなります。

  • テストフレームワークには、グローバルな名前空間をdescribe it expectなどのもので汚染する慣習があります。グローバルな型定義の競合を避けるために、e2eの tsconfig.jsonnode_modulesをこの特別なe2eフォルダに保存することをおすすめします。

e2e/package.jsonファイルにいくつかのスクリプトを追加します:

  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run"
  },

最初のテストをcypress/integration/basic.tsに書きます:

it('should perform basic google search', () => {
  cy.visit('https://google.com');
  cy.get('[name="q"]')
    .type('subscribe')
    .type('{enter}');
});

キーとなるファイルの詳細

e2eフォルダの下に、次のファイルがあります:

  • /cypress.json:Cypressを設定します。デフォルトは空で、必要なのはそれだけです。

  • /cypressサブフォルダ:

    • /integration:あなたのすべてのテスト

      • より良い構成を作るためにサブフォルダの下にテストを作成することは自由です。例:/someFeatureFolder/something.spec.ts

最初のテスト

  • 次の内容の/cypress/integration/first.tsファイルを作成します:

describe('google search', () => {
  it('should work', () => {
    cy.visit('http://www.google.com');
    cy.get('#lst-ib').type('Hello world{enter}')
  });
});

開発中に実行する

次のコマンドを使用してcypress IDEを開きます。

npm run cypress:open

実行するテストを選択します。

ビルドサーバーで実行する

ciモードでCypressテストを実行するには、次のコマンドを使用します。

npm run cypress:run

ヒント: UIとテストの間でコードを共有する

Cypressテストはコンパイル/パックされ、ブラウザで実行されます。プロジェクトコードを自由にテストにインポートしてください。

たとえば、UIセレクタとテストの間でID値を共有して、CSSセレクタが壊れないようにすることができます。

import { Ids } from '../../../src/app/constants';

// Later
cy.get(`#${Ids.username}`)
  .type('john')

ヒント: ページオブジェクトの作成

さまざまなテストがページで行う必要があるすべてのインタラクションに対して便利なハンドルを提供するオブジェクトを作成することは、一般的なテストの慣例です。getterとメソッドでTypeScriptクラスを使用してページオブジェクトを作成できます。

import { Ids } from '../../../src/app/constants';

class LoginPage {
  visit() {
    cy.visit('/login');
  }

  get username() {
    return cy.get(`#${Ids.username}`);
  }
}
const page = new LoginPage();

// Later
page.visit();

page.username.type('john');

ヒント: 明示的なアサーション

Cypressには、ウェブ用のほんのいくつかのアサーションヘルプが付属しています。例えば、chai-jquery https://docs.cypress.io/guides/references/assertions.html#Chai-jQuery です。 それらを使うには、.shouldコマンドを使用して、chainerに文字列として渡します:

cy.get('#foo')
  .should('have.text', 'something')

You get intellisense for should chainers as cypress ships with correct TypeScript definitions 👍🏻

The complete list of chainers is available here : https://docs.cypress.io/guides/references/assertions.html

If you want something complex you can even use should(callback) and e.g.

cy.get('div')
  .should(($div) => {
    expect($div).to.have.length(1);
    expect($div[0].className).to.contain('heading');
  })
// これは単なる例です。普通は`.should('have.class', 'heading')`のように書きます。

ヒント: cypressにはcallbackの呼び出しにも自動リトライ機能があるため、普通の文字列のチェーンと同じように壊れにくいコードを書くことができます。

ヒント: コマンドとチェーン

cypressチェーン内のすべての関数呼び出しはcommandです。shouldコマンドはアサーションです。チェーンとアクションの別々の_カテゴリ_を別々に開始することは慣習になっています:

// Don't do this
cy.get(/**something*/)
  .should(/**something*/)
  .click()
  .should(/**something*/)
  .get(/**something else*/)
  .should(/**something*/)

// Prefer separating the two gets
cy.get(/**something*/)
  .should(/**something*/)
  .click()
  .should(/**something*/)

cy.get(/**something else*/)
  .should(/**something*/)

他の何かのライブラリは、同時にこのコードを評価し、実行します。それらのライブラリは、単一のチェーンが必要になります。それはセレクタやアサーションが混在してデバッグを行うのが難しくなります。

Cypressのコマンドは、本質的に、コマンドを後で実行するためのCypressランタイムへの_宣言_です。端的な言葉:Cypressはより簡単にします

ヒント: より容易なクエリのためにcontainsを使う

下記に例を示します:

cy.get('#foo')
  // Once #foo is found the following:
  .contains('Submit')
  .click()
  // ^ will continue to search for something that has text `Submit` and fail if it times out.
  // ^ After it is found trigger a click on the HTML Node that contained the text `Submit`.

ヒント: スマートディレイとリトライ

Cypressはたくさんの非同期のものに対して、自動的に待ち(そしてリトライし)ます。

// If there is no request against the `foo` alias cypress will wait for 4 seconds automatically
cy.wait('@foo')
// If there is no element with id #foo cypress will wait for 4 seconds automatically and keep retrying
cy.get('#foo')

これにより、テストコードフローに常に任意のタイムアウトのロジックを追加する必要がなくなります。

ヒント: 暗黙のアサーション

Cypressには暗黙のアサーションという概念があります。1つ前のコマンドが原因でそれ移行のコマンドでエラーが発生したときに実行されます。These kick in if a future command is erroring because of a previous command. E.g. the following will error at contains (after automatic retries of course) as nothing found can get clicked:

cy.get('#foo')
  // Once #foo is found the following:
  .contains('Submit')
  .click()
  // ^ Error: #foo does not have anything that `contains` `'Submit'`

伝統的なフレームワークでは、nullにはclickがないというような恐ろしいエラーを目にすることでしょう。Cypressの場合、#fooにはSubmitが含まれていないという親切なエラーが表示されます。このようなエラーは暗黙のアサーションの一種です。

ヒント: HTTPリクエストを待つ

アプリケーションが作るXHRに必要なすべてのタイムアウトが原因となり、多くのテストが脆くなりました。cy.serverは次のことを簡単にします。

  • バックエンド呼び出しのエイリアスを作成する

  • それらが発生するのを待つ

例:

cy.server()
  .route('POST', 'https://example.com/api/application/load')
  .as('load') // create an alias

// Start test
cy.visit('/')

// wait for the call
cy.wait('@load')

// Now the data is loaded

ヒント: HTTPリクエストのレスポンスをモックする

routeを使ってリクエストのレスポンスを簡単にモックすることもできます:

cy.server()
  .route('POST', 'https://example.com/api/application/load', /* Example payload response */{success:true});

ヒント: HTTPリクエストのレスポンスをアサートする

リクエストのアサートは、モックを作らなくてもrouteonRequest/onResponseを使うことで実現できます。

cy.route({
  method: 'POST',
  url: 'https://example.com/api/application/load',
  onRequest: (xhr) => {
    // Example assertion
    expect(xhr.request.body.data).to.deep.equal({success:true});
  }
})

ヒント: 時間をモックする

waitを使ってある時間テストを一時停止することができます。自動的に"あなたはログアウトされます"という通知画面をテストする例:

cy.visit('/');
cy.wait(waitMilliseconds);
cy.get('#logoutNotification').should('be.visible');

しかし、cy.clockと時間をモックし、cy.tclを使用して時間を前倒しすることが推奨されます:

cy.clock();

cy.visit('/');
cy.tick(waitMilliseconds);
cy.get('#logoutNotification').should('be.visible');

ヒント: アプリケーションコードのユニットテスト

あなたはCypressを使ってアプリケーションコードを分離してユニットテストを行うことも可能です。

import { once } from '../../../src/app/utils';

// Later
it('should only call function once', () => {
  let called = 0;
  const callMe = once(()=>called++);
  callMe();
  callMe();
  expect(called).to.equal(1);
});

ヒント: ユニットテストにおけるモック

もしあなたがアプリケーショのモジュールをユニットテストしていたら、あなたはcy.stubを使ってモックを提供することが可能です。例えば、あなたはnavigateが関数fooで呼ばれることを確認できます:

  • foo.ts

import { navigate } from 'takeme';

export function foo() {
  navigate('/foo');
}
  • 下記をsome.spec.tsで行います

/// <reference types="cypress"/>

import { foo } from '../../../src/app/foo';
import * as takeme from 'takeme';

describe('should work', () => {
  it('should stub it', () => {
    cy.stub(takeme, 'navigate');
    foo();
    expect(takeme.navigate).to.have.been.calledWith('/foo');
  })
});

ヒント: コマンド - 実行の分離

たとえば、cy.get('#something')のようなCypressのコマンド(またはアサーション)を呼び出したとき、関数は実際には何もアクションを行わずに即座に返ります。実際に関数が行うのは、Cypressのテストランナーに対して、あるアクション(この場合はget)をある時点で実行する必要があると伝えることです。

あなたが行うことは、基本的には、ランナーが将来実行することになるコマンドリストを書くことになります。このようにコマンドと実行が分離されていることは、次のようなシンプルなテストを書くことで確かめることができます。このテストを実行すると、ランナーがコマンドを_実行_する前に、start / between / endconsole.log文がすぐに実行されることがわかります。

/// <reference types="cypress"/>

describe('Hello world', () => {
  it('demonstrate command - execution separation', () => {
    console.log('start');
    cy.visit('http://www.google.com');
    console.log('between');
    cy.get('.gLFyf').type('Hello world');
    console.log('end');
  });
});

このようなコマンドの実行の分離を行うことには、2つの利点があります。

  • ランナーがコマンドを実行するときに、_脆さに耐えられる_やり方で自動リトライや暗黙のアサーションを実行できる。

  • 非同期に書くコードを同期的に書くことができるため、コードのメンテナンスが難しくなってしまう常に_チェーンする_ような書き方をする必要がなくなる。

ヒント: ブレークポイント

Cypressテストによって生成された自動スナップショット+コマンドログは、デバッグに最適です。とはいえ、それは、あなたが望むならテストの実行を一時停止できます。

まずChrome Developer Tools(愛情を込めてdev toolsと呼ばれています)をテストランナー(macではCMD + ALT + i/windowsではF12)で開いていることを確認してください。一度dev toolsを開けば、あなたはテストをリランすることができ、dev toolsは開いたままになります。もしdev toolsを開いていれば、あなたは2つの方法でテストを実行できます:

  • アプリケーションコードのブレークポイント: debugger文をアプリケーションのコードを使うと、テストランナーは通常のweb開発のように、ちょうどそこで停止します。

  • テストコードのブレークポイント: あなたは.debug()コマンドを使い、cypressのテスト実行をそこで停止できます。例えば、.then(() => { debugger })です。あなたはいくつかのエレメントを得ること(cy.get('#foo').then(($ /* a reference to the dom element */) => { debugger; }))や、ネットワーク呼び出し(cy.request('https://someurl').then((res /* network response */) => { debugger });)すら可能です。しかし、慣用的な方法は、cy.get('#foo').debug()です。そして、テストランナーがdebugで止まったときに、getをコマンドログでクリックすると自動的にconsole.logにあなたが知りたい.get('#foo')に関する情報が出力されます(そして、デバッグに必要な他のコマンドでも似たようなものです)

ヒント: サーバーを開始してテストを実行する

もしテストの前にローカルサーバーを起動したい場合はstart-server-and-test https://github.com/bahmutov/start-server-and-test を依存関係に追加できます。それは次の引数を受け取ります。

  • サーバーを実行するためのnpmスクリプト

  • サーバーが起動しているかをチェックするためのエンドポイント

  • テストを初期化するためのnpmスクリプト

package.jsonの例:

{
    "scripts": {
        "start-server": "npm start",
        "run-tests": "mocha e2e-spec.js",
        "ci": "start-server-and-test start-server http://localhost:8080 run-tests"
    }
}

リソース

最終更新