E2Eテスティングフレームワーク、Cypress,TestCafeを試してみた

E2Eテスティングフレームワーク、Cypress,TestCafeを試してみた

最近、注目されつつある2つのE2Eテスティングフレームワークを試してみました。

Cypress
TestCafe

Cypressは一休さんのブログE2EテストをSelenium Webdriver からCypress.io に移行した話でも述べられており、注目されているように感じます。

2つのE2Eテスティングフレームワークを触ったのでこれらの違いについて述べていきたいと思います。

特徴

2つのテスティングフレームワークの特徴は以下です。

個人的には3つ目の特徴DOMレンダリング完了するまで自動で待機するは便利かなと思います。 今までのE2Eやスクレイピングライブラリではページ遷移後にDOM要素が現れるまで待機する時間を開発者が記述しなければいけませんでした。

しかしながら2つのテスティングフレームワークはそれらを記述しなくても自動で待機してくれます。

Cypress

Cypressのテストの書き方は以下です。

describe('My First Test', function() {
  it('Gets, types and asserts', function() {
    cy.visit('https://example.cypress.io')

    cy.contains('type').click()

    // Should be on a new URL which includes '/commands/actions'
    cy.url().should('include', '/commands/actions')

    // Get an input, type into it and verify that the value has been updated
    cy.get('.action-email')
      .type('fake@email.com')
      .should('have.value', 'fake@email.com')
  })
})

cy.visit('https://example.cypress.io')の記述でhttps://example.cypress.ioのページに遷移しています。 もちろんこの記述だけで、DOM要素がレンダリングされるまで勝手に待っていてくれます。

cy.contains('type').click()では”type”という文字列を含んだ、要素をクリックしています。

cy.url().should('include', '/commands/actions')こちらの構文がテストの構文です。 URLが/commands/actionsを含んでいるかをテストしています。

cy.get('.action-email')でaction-email classのDOM要素を取得し、そのinputフォームに対してtype('fake@email.com')で文字を入力します。 should('have.value', 'fake@email.com')で入力が行われたかをチェックしています。

Cypressの良いところ

Cypressはcypress openのコマンドで起動するとインタラクティブモードで起動できます。 起動するとまず、テストファイルの一覧が表示されます。 実行したいファイル名をクリックするとテストがブラウザ上で実行されます。

cypress_window インタラクティブモードで実行すると動作がブラウザ上で確認できます。 また、テスト実行時に、メソッド1行1行ののスナップショットが保存されます。 テスト終了後、ウィンドウ左に表示されているメソッドを押すとそのメソッドの実行内容がブラウザ場に表示されます。 これが非常に便利です。 cypress_debug インタラクティブモードのウィンドウでDOM要素を選択すると、どのようなPATHでDOMにアクセスすれば良いかが表示されます。(画面上部のcy.get()の部分)

Cypressでハマったところ

ハマったところは以下の2つでした。

1つ目のUTF-8以外の文字コードでエンコーディングされたサイトで文字化けがおこる事象はCypressのGitHub issuesでも議論されています。 例えばShift_JISでエンコーディングされたHTMLに対してcy.visitすると文字化けが起こります。 実際に以下のような記述があるサイトで文字化けが起こりました。

<head>
<meta http-equiv="Content-Type" content="text/html;charset=Shift_JIS">
// 省略
</head>

詳しくはissses を参照してください。

次に、2つ目のDOMの子要素に対しての操作でコードが難解になるについてです。 CypressでDOMの複数の子要素に対して同様の操作を行うときはeach関数を利用します。実際にeachを使用したコードが以下です。

cy.get(".calendars > :nth-child(3) > table > tbody > tr").each(tr => {
       return cy.wrap(tr.children()).each(elment => {
         if (elment.hasClass("class0001") || elment.hasClass("class0003")) {
           tdCount += 1;
         }
         if (tdCount === 7) {
           cy.wrap(elment).click();
           return;
         }
       });
     });

eachがネストしたりするとコードが読みにくくなります。 for文のネストのようなものですが、DOM要素をたどるプログラムでは上のようなコードが頻出するのではないかと思います。読めなくはないですが、理解するのに時間がかかってしまいそうです。

TestCafe

続いてTestCafeです。 TestCafeのテストの書き方は以下です。

import { Selector } from 'testcafe';

fixture `Getting Started`
    .page `http://devexpress.github.io/testcafe/example`;

test('My first test', async t => {
    await t
        .typeText('#developer-name', 'John Smith')
        .click('#submit-button')

        // Use the assertion to check if the actual header text is equal to the expected one
        .expect(Selector('#article-header').innerText).eql('Thank you, John Smith!');
});

page関数で遷移するページを指定しています。 今回はhttp://devexpress.github.io/testcafe/exampleに遷移しています。

t.typeTextで#developper-nameのinputフォームに”John Smith”を入力し、clickで#submit-buttonのsubmitボタンをクリックします。 最後のexpecteqlで#article-headerのDOMの文字列が”Thank you, John Smith!“であるかをテストしています。 こちらもjestのような書き方でテストがかけます。 Cypressと違うところは各関数がPromiseを返すという点です。 Selector関数でDOM要素を取得できます。

TestCafeの良いところ

TestCafeにもLive ModeというCypressのインタラクティブモードに似た機能が備わっています。 Live Modeで起動するにはtestcafe chrome test.js -Lのように-Lオプションをつけて実行します。 実行すると以下のようなウィンドウが起動します。 こちらもテストがブラウザ上で実行されているのを確認することができます。 また、DOM要素の属性を知りたい場合は通常のChrome同様、検証(F12)からDOM要素をクリックし、属性を確認します。

また、先ほども述べましたがTestCafeの各関数はPromiseを返します。JSに慣れている人であれば普通のライブラリを使うような感覚でプログラムを書くことができるます。

環境変数がprocess.env.HOGEHOGEで取得できるのも地味にありがたかったです。

TestCafeでハマったところ

Client上でコードを実施したい場合はClietFunctionという関数を使う必要があります。

import { Selector } from 'testcafe';
import { ClientFunction } from 'testcafe';

fixture `My Fixture`
    .page `examplepage.html`;

    const testedPage = Selector('#testedpage');

    const getChildNodeText = ClientFunction(index => {
        return getEl().childNodes[index].textContent;
    },{ dependencies: { getEl: testedPage } });

test('My Test', async t => {
    await t.expect(getChildNodeText(0)).contains('This is my tested page.');
});

ClientFunctionを使うと慣れ親しんだDOM操作メソッドを記述することが可能です。ただ、この関数はTypeScriptの型定義がしっかりとできていなかったせいか、 dependenciesを使ったところでコンパイルエラーになってしまいました。

どっちが良いのか?

デバッグの観点からいくとCypressは優れているなと感じました。インタラクティブモードを使って実装を進めれば、DOM取得のプログラムが非常に簡単に実装できます。

しかしながら、コードの書きやす的にはTestCafeの方が書きやすかったです。 TestCafeの方が直感的にわかりやすいコードになるので、コードレビューもしやすそうです。

しかながら、どちらも使いやすツールなので、実際に使ってみて決めるのが良いと思います。