cypress を使って自動テストを簡単に導入する方法

![新規cypressウィンドウ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/cea51d8b-19b8-d569-e22d-ff6440920af0.png)
![自動テストの様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/e18142db-d249-5226-08a7-0a26b3d5c372.png)

[2023年版はこちら](https://qiita.com/Takakiri/items/b7011eb1949371f887e0)

更新: 2021-03-13
- cypress 6.6.0 に更新


## cypress のインストール

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

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

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

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

![Node.js インストール ページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/1d89fa0e-95bf-2d3d-d6e3-4f5dfd2e9f70.png)


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


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

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

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

- タスクバーにあるフォルダー（エクスプローラー）を右クリック >> デスクトップ
- 開いたエクスプローラー >> ファイル（メニュー） >> Windows PowerShell を開く
- （必要なら）タスクバーに表示された PowerShell を右クリックしてピン留めする

以上で PowerShell が開きます。
PowerShell の中で以下のコマンドを入力して、セキュリティを中レベルに緩める必要があります。

    Set-ExecutionPolicy  RemoteSigned  -Scope CurrentUser

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


### cypress をインストールします

- プロジェクト フォルダーを新規作成します。 例：デスクトップ/try_cypress
- プロジェクト フォルダーを開き >> ファイル（メニュー） >> Windows PowerShell を開く

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

    npm install cypress

インストールが完了したら、シェルを閉じます。

なお、npm install に -g オプションを付けると警告されます。
%USERPROFILE%\AppData\Roaming\Cypress に設定ファイルができます。


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

- プロジェクト フォルダーを開き >> ファイル（メニュー） >> Windows PowerShell を開く

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

    npx cypress open

しばらく待つと __Project__/cypress.json ファイルと __Project__/cypress フォルダーができ、
cypress のウィンドウが開きます。
初回ははじめかたのガイドが表示されますが、そのまま OK, got it! ボタンを押します。
cypress のウィンドウには、__Project__/cypress/integration フォルダーの内容が表示されます。

![新規cypressウィンドウ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/25ee3bbf-e43f-d6e1-b39c-19e3ef2f0c5d.png)

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


### cypress の初期設定をします

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

cypress のウィンドウから開くテキスト エディターのパスを
cypress >> Settings（タブ）>> File Opener Preference >> Other に設定します。

![新規cypressウィンドウ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/efdefc3f-5f5f-2a59-9348-65309225fdbe.png)

- メモ帳の場合: C:\Windows\notepad.exe
- Visual Studio Code の場合: %USERPROFILE%\AppData\Local\Programs\Microsoft VS Code\Code.exe

上記 %USERPROFILE% の部分は環境変数ですが、cypress には環境変数の値を設定します。
例： C:\Users\user1

%USERPROFILE% の値を調べるには、
- %USERPROFILE% という文字列をコピー
- Windows キーを押す
- Ctrl + V で貼り付け
- %USERPROFILE% フォルダーを開く
- 開いたフォルダーのアドレスバーをクリック


### cypress の動作確認をします

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

- cypress のウインドウで、たとえば、actions.spec.js をクリックするとその自動テストが始まります
- *.js の右にマウス カーソルを移動させて Open IDE を選ぶと、テキスト エディターが開きます
- *.js ファイルを上書き保存すると、自動的にテストが再起動します

![自動テストの様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/c04012a2-58e7-87ec-6545-3db5e098ca64.png)

動作確認をしたら、__Project__/cypress/integration/example フォルダーを削除します。


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

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

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

![test_target_1.html](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/d1a29111-6530-74f7-4f74-2607f2f4cddb.png)

テスト対象となる HTML ファイルを以下のように作成します。
Visual Studio Code などをインストールして編集するとよいでしょう。

__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>

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

__Project__/cypress/integration/test1.spec.js

    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 のウインドウで、test1.spec.js をクリックすると自動テストが始まります。
テストは成功します。

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

__Project__/cypress.json

    {
        "viewportWidth": 480,
        "viewportHeight": 320
    }


### 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 のパラメーターが作られます）
- コピーボタンを押す（下記③）
- テストコードに貼り付ける

![Selector Playground](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/c1c5482c-a757-b030-f8bc-2c6418c315d5.png)

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

    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')


## デバッグ表示

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

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

![log](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/57efc74d-af76-0958-d383-069bb29e101c.png)

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

    it('', () => {
        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,  value ) {
        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, b) {
        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 部品のテストコードを作ってみましょう。

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

![test_target_2.html](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427559/a02fbbb1-b644-9304-8ab6-aae838c0ac5c.png)

テスト対象となる 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/integration/test2.spec.js

    describe('My Second Test', () => {
        before(() => {
            cy.visit('test_target_2.html')  // または http://localhost:3000 など
        })
        beforeEach(() => {})
    
        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) {
        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 が３つある場合は、下記の順番で実行します。

- 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 という項目がある行を検索し、行番号のマイナス１が 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 属性に指定できる複数の値のうち、１つでも 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('手動で～の操作をしたら、続行してください。参考：エクセルの p5')
        cy.pause()
        // check ...
    }

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

    {
        "env": {
            "manualTest": "true",
            "testUserName": "Bob Ross",
            "testUserPassword": "Bobbbbb",
        }
    }


## 止まってしまうとき

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


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

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

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

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

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


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

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

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

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

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

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