🚴‍♀️

cypress を使って自動テストを簡単に導入する方法(2023年版)

2023/05/07に公開


 

更新: 2023-05-08

  • cypress 12.11.0 に更新

古いバージョンの cypress の記事

cypress のインストール

以後のインストール手順については、Windows 11 22H2 で動作確認しています。

Node.js をインストールします

cypress が使う Node.js をインストールします。

https://nodejs.org/ja/download/ をブラウザーで開き、Windows Installer (.msi) >> 64-bit をダウンロードして、実行します。ダウロードするファイルの名前は node-v18.16.0-x64.msi のようになっています。

インストール オプションはすべてデフォルトで構いません。

PowerShell を開き、PSスクリプトを実行できるようにします

cypress を PowerShell から起動できるようにするには、PowerShell のセキュリティの設定を少し緩める必要があります。

他のシェルを使うときはこの手順は不要です。

  • Windows スタート >> po(と入力)>> PowerShell

以上で PowerShell が開きます。必要ならタスクバーに表示された PowerShell を右クリックしてピン留めします。PowerShell の中で以下のコマンドを入力して、y を入力します。こうしてセキュリティを中レベルに緩める必要があります。

Set-ExecutionPolicy  RemoteSigned  -Scope CurrentUser

以後、PowerShell のことを単に「シェル」と呼ぶことにします。

cypress のプロジェクトを新規作成します

プロジェクト フォルダーを新規作成して、そこに移動します。以下のコマンドを入力するか、エクスプローラーで新規作成してそのフォルダーを cd と入力したシェルにドラッグ&ドロップします。例:デスクトップ/try_cypress

mkdir  $HOME\Desktop\try_cypress
cd  $HOME\Desktop\try_cypress

cypress のプロジェクトを新規作成します。

PowerShell の場合、以下のコマンドを入力します。

npm init -y
npm install cypress typescript
npx tsc --init --types cypress --lib dom,es6
echo "import { defineConfig } from 'cypress'" | Out-File "cypress.config.ts" -Encoding ascii
echo "export default defineConfig({})" | Out-File -Append "cypress.config.ts" -Encoding ascii
npx cypress open

Git bash の場合、以下のコマンドを入力します。

npm init -y
npm install cypress typescript
npx tsc --init --types cypress --lib dom,es6
echo "import { defineConfig } from 'cypress'" > cypress.config.ts
echo "export default defineConfig({})"       >> cypress.config.ts
npx cypress open

もし、Windows Defender の警告が表示されたら許可してください。

新機能を紹介するウィンドウが開きます。

Continue ボタン を押すと、cypress の初期ウィンドウが開きます。

E2E Testing(以前のバージョンからあるテスト)を選びます。なお、本書では Component Testing については説明していません。

コンフィギュレーション ファイル に関しては、そのまま Continue ボタンを押します。

テストに使うブラウザーを選びます。Chrome や Firefox がインストールしてあれば、それらも選択候補に表示されます。

選んだブラウザーが開き、最初の仕様(テスト)として多くの テスト サンプルを作るか、1つシンプルなテストを作るかを選ぶページが表示されます。ここではまず Scaffold example specs(左)を選びます。

作られるファイルが表示されるので、そのまま OKay, I got it! ボタンを押します。

テストの一覧が表示されます。ファイル名をクリックするとテストが実行されます。すべてのテストを一度に実施することはこの画面ではできないようです。プロジェクト フォルダー を GUI の無い Linux に移動しても node_modules を作り直してシェルから実行すれば実施できますが、本書では具体的な手順は説明していません。

cypress のウィンドウには、Project/cypress/e2e フォルダーの内容が表示されます。

cypress のウィンドウを閉じて、再び開くときは npx cypress open コマンドを実行します。

(参考)既存の cypress のプロジェクトをインストールします

git clone するなどして cypress のプロジェクトをインストールした場合、cypress を起動する前に node_modules フォルダーを復帰させる必要があります。

シェルに以下のコマンドを入力します。

Project/package-lock.json ファイルがある場合:

cd __Project__
npm ci
npx cypress open

Project/yarn.lock ファイルがある場合:

cd __Project__
yarn install --frozen-lockfile
npx cypress open

この場合、cypress のバージョンは package.json に書かれている cypress のバージョンになります。

cypress の初期設定をします

初めて cypress のウィンドウを開いたときは、cypress の初期設定を行います。ただ、初期設定をしなくてもほとんどの機能は使えるのでスキップできます。

cypress のウィンドウから開くテキスト エディターのパスは、cypress に制御されているブラウザー >> Settings(左)>> Device Settings >> External Editor = Custom を選び、エディターのパスを設定します。

  • メモ帳の場合: C:\Windows\notepad.exe
  • Visual Studio Code の場合: C:\Users\user1\AppData\Local\Programs\Microsoft VS Code\Code.exe

上記 user1 の部分は、シェルで環境変数 USERPROFILE の値を表示させて、置き換えてください。

PowerShell の場合:

PS> echo ${env:USERPROFILE}
C:\Users\user1    ... 例

Git bash の場合:

$ echo ${USERPROFILE}
C:\Users\user1    ... 例

cypress の動作確認をします

cypress のプロジェクトを新規作成すると、テストコードのサンプルも作成されるので、それを動かしてみましょう。

  • cypress のウインドウで、たとえば、todo.cy.js をクリックするとその自動テストが始まります

  • 自動テストを実行するページで、*.js の右にマウス カーソルを移動させて Open IDE を選ぶと、テキスト エディターが開きます
  • *.js ファイルを上書き保存すると、自動的にテストが再起動します

以下のエラーが出るときは、前述のように PSスクリプトを実行できるようにして、node_modules フォルダーを削除し、npm install cypress typescript からやり直してください。

Error: Webpack Compilation Error
[tsl] ERROR
TS18002: The 'files' list in config file 'tsconfig.json' is empty.

テストを削除します

次の章ではテストを 1から作っていくので、今までに作られたテストを削除します。

  • cypress の制御下にあるブラウザーを閉じます
  • cypress のウィンドウを閉じます
  • サンプルが入っている、Project/cypress フォルダーを削除します
  • 以下のコマンドで Project/cypress.config.ts ファイルを初期化します

PowerShell の場合、以下のコマンドを入力します。

cd __Project__
echo "import { defineConfig } from 'cypress'" | Out-File "cypress.config.ts" -Encoding ascii
echo "export default defineConfig({})" | Out-File -Append "cypress.config.ts" -Encoding ascii

Git bash の場合、以下のコマンドを入力します。

echo "import { defineConfig } from 'cypress'" > cypress.config.ts
echo "export default defineConfig({})"       >> cypress.config.ts

基本的な自動テストを作る

以下の基本的なテストコードを作ってみましょう。

  • 入力項目にテストデータを入力する
  • ボタンを押す(入力した英文字を小文字に変換した出力値を表示します)
  • 出力値が正しいことをチェックする

テスト対象となる HTML ファイルを以下のように作成します。Visual Studio Code などをインストールして編集するとよいでしょう。メモ帳を使う場合は、保存されたファイルの拡張子が .txt になっていないことを確認してください。

Project/test_target_1.html

<!DOCTYPE HTML><html><head>
<meta charset="UTF-8">
<title>cypress のサンプル 1</title>
</head>
<body>

    <input type="text" id="input-text"/>
    <input type="button" id="input-button" value="入力" onclick="onButton()"/>
    <div id="result">未入力</div>

    <script>
    function onButton() {
        document.getElementById('result').textContent =
            document.getElementById('input-text').value.toLowerCase();
    }
    </script>
</body>
</html>

最初のテストコードを新しく作ります。前の章で説明したように cypress を起動して E2E Testing を選び、今度は以下のページで Create new spec を選びます。

ファイル名を指定します。ここでは、既定のファイル名のままで Create spec ボタンを押します。

作られたファイルの内容が表示されます。ここではすぐに編集するため、閉じるボタン(右上)を押します。

以上の方法ではなく、Project/cypress/e2e フォルダーの中にファイルを作ってもテストコードとして認識されます。

テストコードは以下のようにします。

Project/cypress/e2e/spec.cy.ts

describe('My First Test', () => {
    it('Input test', () => {
        cy.visit('test_target_1.html')  // または http://localhost:3000 など

        cy.get('#input-text').clear().type('ABC')  // ABC と入力する
        cy.get('#input-button').click()  // ボタンを押す
        cy.get('#result').should('have.text', 'abc')  // 出力をチェックする
        cy.get('#input-text').should('have.value', 'ABC')  // input タグの場合
    })
})

テストコードは通常の JavaScript と違って見えますが、describe, it は分岐やループではなく、すぐに () => {...} の部分を呼び出す関数の呼び出しであるため、通常の JavaScript と同様に上から下に実行されます。詳細は下記で説明します。

テストを実行する、ページのサイズを変える

cypress の制御かにあるブラウザーのウインドウで、spec.cy.ts をクリックすると自動テストが始まります。テストは成功するでしょう。

cypress の制御下にあるブラウザーにはページ全体が縮小表示されます。ページのサイズを調整するには、cypress.config.ts に viewportWidth と viewportHeight を追記します。

Project/cypress.config.ts

import { defineConfig } from "cypress";
export default defineConfig({
    e2e: {
        viewportWidth: 480,
        viewportHeight: 320,
        setupNodeEvents(on, config) {
            // implement node event listeners here
        },
    },
});

保存したら自動的にページのサイズが変わって再テストが行われます。もしうまくいなかかったら、以下を試してください。

  • cypress の制御下にあるブラウザーを閉じます
  • cypress のウィンドウを閉じます
  • npx cypress open コマンドで再起動します

cy.visit - ページを開く

このサンプルでは、Web サーバーを使っていません。その場合、HTML ファイルがあるパスを、cypress フォルダーがあるフォルダーからの相対パスで指定します。

cy.visit('test_target_1.html')

ローカルにある Web サーバーをテストするときは、localhost の URL を指定します。

cy.visit('http://localhost:3000')

インターネットにある Web サーバーをテストするときは、URL を指定します。

cy.visit('https://example.com/')

cy.get - GUI部品を選択する

操作やチェックをする対象となる GUI部品を特定するときは、cy.get メソッドに CSS セレクターを指定するのですが、cy.get メソッドをコーディングする必要はありません。cypress の Selector Playground というツールを使えば cy.get メソッドとそのパラメーターが書かれたコードがクリップボードにコピーされるからです。

ただし、Selector Playground を使う前に HTML に id 属性または data- 属性(例:data-test 属性)を記述しておきます。id 属性等は必須ではありませんが id 属性等が記述してあると、シンプルで変化に強いコードになります。

<input type="text" id="input-text"/>

または

<input type="text" data-test="input-text"/>

cypress の制御下にあるブラウザーで Selector Playground が使えます。

  • テストを開始
  • Selector Playground のボタンを押す(下記①) そのボタンのすぐ下にある矢印ボタンが灰色になっていたらクリックして青色に変えてください
  • 対象となる GUI部品をクリック(下記②)(cy.get のパラメーターが作られます)
  • コピーボタンを押す(下記③)
  • テストコードに貼り付ける

貼り付けられるコードは以下のようになります。

cy.get('#input-text')

または

cy.get('[data-test=input-text]')

CSS を使うときの CSS セレクターは、class 属性を指定することが多いですが、cypress などのテストコードには class 属性を指定しないでください。id 属性または data- 属性を指定してください。なぜなら CSS の都合で class 属性の値が編集されたときに cy.get が失敗してしまうからです。

// cy.get('.input-text')  class 属性の指定は禁止

id 属性が対象となる GUI 部品を表す ID として適切ではない値がすでに記述されている場合や、id 属性が重複していている場合など、後で修正する可能性がある場合は、data- 属性を cy.get に渡すようにすると、id 属性が修正されたときの影響を受けなくて済むようになります。

cy.find - GUI部品を絞り込む

サンプルには有りませんが、find メソッドを使うと get メソッドの要素からさらに絞り込むことができます。

cy.get('#table-id').find('td')

ただし、CSS セレクターでも子要素を選択できる場合は、find を使わずに済みます。

cy.get('#table-id  td')

should have.text, should have.value - 値をチェックする

テストをすることは、出力値が正しい値(期待する値)であることをチェックすることです。

HTML タグの間のテキストをチェックするテストコードと、input タグの中のテキストをチェックするテストコードは、以下のように若干異なるので注意してください。

HTML タグの間のテキストをチェックする

cy.get('#result').should('have.text', 'abc')

input タグの中のテキストをチェックする

cy.get('#input-text').should('have.value', 'ABC')

デバッグ表示

JavaScript の console.log メソッドによるログ出力はできますが、コンソールはアサーション(チェック)のログとは別のビューなので前後のタイミングの関係が分からないという問題があります。また、オブジェクトを console.log で表示すると、展開したときのタイミングのプロパティの値が表示されてしまい、console.log を呼び出したタイミングのプロパティの値が分かりません。

アサーションのログを応用した下記の log 関数を使うことで log を呼び出したタイミングのプロパティの値をアサーションのログの中に表示させることができます。

なお、下記 log 関数は cypress が提供している関数ではなく独自に作成した関数です。

Project/cypress/e2e/spec.cy.ts

describe('My First Test', () => {
    ...

    it('log test', () => {
        const  obj = {a:1, b:2}

        log(1, obj)
        console.log(obj)
        obj.a = 3  // これにより console.log では {a:3, b:2} と表示される
    })
})

// log shows "assert expected __label__ to not equal __value__"
// e.g. log(1, variable)
function  log( label: any,  value: any ) {
    if (typeof value === 'object') {
            expect( label ).to.not.eq( recursiveAssign( {}, value ) )
    } else if ( label === value ) {
            expect( label ).to.eq( value )
    } else {
            expect( label ).to.not.eq( value )
    }
}

// recursiveAssign is nested Object.assign
function  recursiveAssign(a: any, b: any) {
    const bIsObject = (Object(b) === b);
    if (!bIsObject) {
            return b;
    }

    const aIsObject = (Object(a) === a);
    if (!aIsObject) {
            if (b instanceof Array) {
                    a = [];
            } else {
                    a = {};
            }
    }
    for (const key of Object.keys(b)) {
            a[key] = recursiveAssign(a[key], b[key]);
    }
    return a;
}

よく使われる GUI 部品の自動テストを作る

以下のよく使われる GUI 部品のテストコードを作ってみましょう。

  • チェックボックスを操作する
  • チェックボックスのチェック状態をチェックする
  • ドロップダウンリストを操作する
  • ドロップダウンリストの選択状態をチェックする
  • 表の中のテキストをチェックする

テスト対象となる HTML ファイルを以下のように作成します。

Project/test_target_2.html

<!DOCTYPE HTML><html><head>
<meta charset="UTF-8">
<title>cypress のサンプル 2</title>
</head>
<body>

    <input type="checkbox" id="input-check"/>チェック

    <select id="input-drop-down">
        <option value="a">選択肢1</option>
        <option value="b">選択肢2</option>
        <option value="c" selected>選択肢3</option>
    </select>

    <table id="result-table" style="border-style: solid">
        <tr><td id="table-id-0">A</td><td id="table-value-0">100</td></tr>
        <tr><td id="table-id-1">B</td><td id="table-value-1">200</td></tr>
        <tr><td id="table-id-2">C</td><td id="table-value-2">300</td></tr>
    </table>

</body>
</html>

テストコードは以下のようになります。

Project/cypress/e2e/spec2.cy.ts

describe('My Second Test', () => {
    before(() => {})
    beforeEach(() => {
        cy.visit('test_target_2.html')  // または http://localhost:3000 など
    })

    it('check box test', () => {
        cy.get('#input-check').click()
        cy.get('#input-check').should('be.checked')  // be.not.checked
    })

    it('drop down list test', () => {
        cy.get('#input-drop-down').select('b')
        cy.get('#input-drop-down').should('have.value', 'b')
    })

    it('Table test', () => {
        getRowIndex(cy.get('#result-table  td').contains('B')).then((iRow) => {
            cy.get(`#table-value-${iRow}`).should('have.text', '200')
        })
    })
    afterEach(() => {})
    after(() => {})
})

// getRowIndex
function  getRowIndex(cellElement: any) {
    return  cellElement.parentsUntil('tbody').last().invoke('index')
}

describe, it, before, beforeEach, afterEach, after - テストコードの構成

テストコードは通常の JavaScript と違って見えますが、 describe, it は分岐やループではなく、すぐに () => {...} の部分を呼び出す関数の呼び出しであるため、 通常の JavaScript と同様に上から下に実行されます。

厳密には、() => { ... } の中の部分は非同期に実行され、
() => { ... } の外の部分が先に実行されるのですが、
() => { ... } の中だけ見れば上から下に実行します。

定数データの定義やループは、() => { ... } の外に記述することもできます。ループの中の () => { ... } の中の部分も繰り返し実行されます。

describe はテストケースのグループに相当します。

beforeEach の () => { ... } の中は、それぞれの it の () => { ... } の中を実行する直前に実行します。
afterEach の () => { ... } の中は、それぞれの it の () => { ... } の中を実行した直後に実行します。
before の () => { ... } の中は、最初の beforeEach の () => { ... } の中を実行する直前に実行します。
after の () => { ... } の中は、最初の afterEach の () => { ... } の中を実行する直前に実行します。

つまり、it が3つある場合は、下記の順番で実行します。

  • before
  • beforeEach
  • it
  • afterEach
  • beforeEach
  • it
  • afterEach
  • beforeEach
  • it
  • afterEach
  • after

describe はネストすることもできます。ネストするとテストのレポートがネストして表示されますが、ネストに関係なく通常の JavaScript と同様に上から下に実行します。

describe や it に .only を付けると、その部分だけ実行します。ただし、その前後の before, beforeEach, afterEach, after も実行します。

it.only('check box test', () => {

describe や it に .skip を付けると、その部分だけ実行しません。

it.skip('check box test', () => {

表の中のテキストをチェックする

行の順番が重要ではない表形式で出力されるとき、行が入れ替わって出力されることがあります。

そのテストは、チェック対象の行を検索してから、見つかった行の中の項目の内容をチェックするとよいでしょう。下記のコードは、B という項目がある行を検索し、行番号のマイナス1が iRow 引数に渡され、iRow を使った id 属性の項目のテキストをチェックしています。B という項目は 2行目にあるので iRow は 1 になります。id="table-value-1" の項目のテキストが 200 ならテストは成功します。なお、getRowIndex 関数は cypress が提供している関数ではなく独自に作成した関数です。

getRowIndex(cy.get('#result-table  td').contains('B')).then((iRow) => {
    cy.get(`#table-value-${iRow}`).should('have.text', '200')

その他よく使われるメソッド

href 属性が /users であることをチェックします

cy.get(____).should('have.attr', 'href', '/users')

src 属性が指定の文字列を含むことをチェックします(画像のファイル名)

cy.get(____).should('have.attr', 'src').and('contain', 'partOfString')

src 属性が指定の正規表現にマッチすることをチェックします

cy.get(____).should('have.attr', 'src').and('match', /regularExpression/)

CSS display による非表示を待つ

cy.get(____).should('have.css', 'display', 'none')

class 属性に指定できる複数の値のうち、1つでも disbled があることをチェックします

cy.get(____).should('have.class', 'disbled')

表示されるまで待ちます

cy.get(____).should('be.visible')

フォーカスされるまで待ちます

cy.get(____).should('have.focus') 

手動テスト

自動化することが難しい一部の操作に関しては、手動で操作したり目視でチェックしたりするとよいでしょう。

cy.pause()

cy.pause() を呼び出すと、ブラウザーの動作は停止し、cypress のウィンドウの左上に続行ボタンが表示されます。

ただし、ブラウザーの動作が停止すると、クリックなどをしたときの反応も動作しなくなってしまいます。反応を動作させるには、pause するのではなく、それ以降の処理や終了処理(after) をコメントアウトしてください。ページ移動が伴うときは、ページが移動されるのを待ってから pause してください。

手動テストで操作する内容をガイドしてから pause するとよいでしょう。

const  manualTest = (Cypress.env('manualTest') === 'true')
if (manualTest) {
    log('','手動で~の操作をしたら、続行してください。参考:エクセルの No.5 シート')
    cy.pause()
    // check ...
}

Cypress.env は cypress.config.ts ファイルの env フィールドを参照します。

export default defineConfig({
    env: {
        "manualTest": "true",
        "testUserName": "Bob Ross",
        "testUserPassword": "Bobbbbb",
    }
}

止まってしまうとき

テストコードに対してブラウザーは非同期で動作するため、対象の GUI 部品をうまく捕捉できないで止まることや操作やチェックができないことがあります。そのあたりのノウハウについては、別の記事で説明しています。

cypress で期待通りに自動テストが動かなかったときの対策集

E2Eテストを自動化して早期にバグを無くす

cypress を使うことで Web アプリケーションの E2Eテストを簡単に自動化することができます。

自動テストを行うと早期にバグが見つかり、開発中のデグレードが発生する可能性が下がり、開発をスムーズに進めることができるようになります。Web アプリケーションの場合、結合テストは、E2E(End to End)Test とも呼ばれ、エンド ユーザーが行う操作と同じようなテストケースを動かして正しい結果が出力されることをチェックするテストです。 それに対して、ユニットテストは、プログラミング言語(関数やクラス)に対してテストケースを動かして正しい結果が出力されることをチェックするテストです。

ユニットテストだけしている人は多いと思いますが、テスト済みのユニットを集めたシステムでもバグが発生します。ユニット以外の結合部分のコードの量は同じぐらいあるので同じぐらいバグが発生する可能性があります。結合するだけだからバグは存在しないと考えているなら間違いです。開発終盤にライブラリやフレームワークをバージョンアップしないですよね。

結合テストをすることで、エンドユーザーにとって基本的な動作が正しいことを実証できます。 ユニットテストはいくら厳密にしても細かい部分しか実証されません。むしろ、結合テストをすれば、エンドユーザーの視点で細かい部分のバグしか残らないので、結合テストを優先すべきとも考えられます。

自動テストを作るのは難しくない

自動テストのコードを書くのは難しいと思われるかもしれませんが、ほとんどのコードはコピペするだけで作ることができます。Web アプリケーションはゲームと異なり、項目を入力してボタンを押すという単純な操作が大半を占めるので、いくつかのパターンをコピペして使えるようになればほとんどのテストケースを自動化することができるようになります。

コードレスで自動テストが作れるツールもありますが、cypress には Selector Playground というツールがあります。操作やチェックをしたい GUI 部品(ウィジェット)をクリックするだけでGUI 部品を表すコードをコピペできます。後は、GUI 部品を表すコードに、操作やチェックするメソッドを、サンプルコードからコピペしてパラメーターを編集するだけです。コードレスと異なり、テストコードは git でバージョン管理もできます。コードの言語は、JavaScript です。TypeScript も使えますがあまりメリットはありません。

cypress を使って自動テストを作れるようになったスキルは、今後も活用できるでしょう。なぜなら cypress は HTML に対して自動化するため、フロントエンドのフレームワークやプログラミング言語が変わっても HTML 部分は変わないからです。

細かい部分まで完全に自動化しようとすると大変ですが、難しい部分は手動で操作するようにすることもできます。完全自動化することは楽しくクールですが、割り切ることも大事です。

また、テストは網羅的である必要があると考えがちですが、ほとんどのコードは2つのテストデータだけ通れば、ほとんどのバグはなくなります。境界値テストは境界を超える可能性が高いケースだけで十分です。テストは入力値を与えて動作させ、出力値が正しい値と合っているかをチェックするわけですが、バグがあるのに出力値と正しい値が等しくなる可能性は非常に少ないです。網羅的にチェックする必要があるロジックについて関数やクラスに抽出し、それに対して網羅的なユニットテストをするとよいでしょう。ライブラリでテスト済みのケースも自分がテストする必要はありません。

Discussion