Cypress

Last updated 2 months ago

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

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

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

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

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

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

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

インストール

このインストールプロセスで提供される手順は、あなたの組織のボイラープレートとして使用できる素敵なe2eフォルダを提供します。このe2eフォルダをCypressでテストしたい既存のプロジェクトに貼り付けてコピーすることができます

e2eディレクトリを作成し、cypressとその依存関係をTypeScriptのトランスパイルのためにインストールします。

mkdir e2e
cd e2e
npm init -y
npm install cypress webpack @cypress/webpack-preprocessor typescript ts-loader

ここでは特にサイプレスのために別々のe2eフォルダを作成するいくつかの理由があります:

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

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

TypeScriptのtsconfig.jsonを設定します:

{
"compilerOptions": {
"strict": true,
"sourceMap": true,
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6"
],
"jsx": "react",
"experimentalDecorators": true
},
"compileOnSave": false
}

Cypressの最初のdry runを行い、Cypressのフォルダ構造を準備します。 Cypress IDEが開きます。ウェルカムメッセージが表示されたらそれを閉じることができます。

npx cypress open

e2e/cypress/plugins/index.jsを次のように編集して、CypressをTypeScriptのトランスパイルようにセットアップします:

const wp = require('@cypress/webpack-preprocessor')
module.exports = (on) => {
const options = {
webpackOptions: {
resolve: {
extensions: [".ts", ".tsx", ".js"]
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
options: { transpileOnly: true }
}
]
}
},
}
on('file:preprocessor', wp(options))
}

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

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

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

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

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

  • /cypressサブフォルダ:

    • /fixtures:テストフィクスチャ

      • example.jsonが付属しています。削除しても構いません。

      • 単純な.jsonファイルを作成して、複数のテストで使用するサンプルデータ(フィクスチャ)を提供することができます。

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

      • examplesフォルダがあります。安全に削除することができます。

      • .spec.tsで名前を付けます。例:somthing.spec.ts

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

最初のテスト

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

/// <reference types="cypress"/>
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');

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

Cypresssコマンドが失敗したときには、(他の多くのフレームワークではnullのようなものではなく)素晴らしいエラーが発生するので、すばやく失敗し、テストが失敗したときを正確に知ることができます。

cy.get('#foo')
// If there is no element with id #foo cypress will wait for 4 seconds automatically
// If still not found you get an error here ^
// \/ This will not trigger till an element #foo is found
.should('have.text', 'something')

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

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

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

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

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

// Don't do this
cy.get(/**something*/)
.should(/**something*/)
.click()
.should(/**something*/)
.get(/**something else*/)
.should(/**something*/)
// Prefer seperating the two gets
cy.get(/**something*/)
.should(/**something*/)
.click()
.should(/**something*/)
cy.get(/**something else*/)
.should(/**something*/)

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

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

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

下記に例を示します:

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

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

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はたくさんの非同期のものに対して、自動的に待ち(そしてリトライし)ます。

// 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
cy.get('#foo')

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

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

あなたは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);
});

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

もしあなたがアプリケーショのモジュールをユニットテストしていたら、あなたは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');
})
});

TIP: ブレークポイント

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')に関する情報が出力されます(そして、デバッグに必要な他のコマンドでも似たようなものです)

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

もしテストの前にローカルサーバを起動したい場合は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"
}
}

リソース