TestCafeという自動テストフレームワーク(いわゆるE2Eテストフレームワークジャンルとしておきます)を試してみました。

TestCafeの特徴としては、Seleniumを使っていないこと、設定ファイルなしで利用できる点です。

Seleniumを使ったテストフレームワークとしては、ProtractortestiumWebdriverIOなどがあります。 (Seleniumを使わない他のテストフレームワークだとNightmareなど)

Seleniumを使わずにどうやって自動的にブラウザを操作するかというと、中継サーバーを起動してそこにテストコードなどを追加して動かすことができるSelenium RC(1)方式に近いものだと思います。

同名のウェブサービス/クライアントアプリも出していて、こちらはSelenium IDEのようにGUIで操作して記録したものを再生などができます。

なぜ、Seleniumを使っていないかという点については、以下で回答されています。

簡単にまとめると次のような点をどうにかしたいので、Seleniumを使わずに動く自動テストフレームワークを作ったようです。

  • Remoteの端末でも動かせるようにしたい
  • とにかく設定なしでうごかせるようにしたい
  • WebDriverの互換性がブラウザ依存で、それが解決されるまでに時間かかる

実際に動かしながら見ていきましょう。

最小のデモ

以下に最小のデモプロジェクトを作りました

これをインストールから動かすまでは次のコマンドを叩くだけです。

git clone https://github.com/azu/demo-test-cafe.git
cd demo-test-cafe
npm install
npm test

これだけでChromeが立ち上がり、自動テストが実行できます。

via GIPHY

テストコードはTestCafeにBabelが内蔵されているため、ES2016+async/awaitなどがそのまま書けるようになっています。

const assert = require("assert");
import {Selector} from 'testcafe';
const getElementById = Selector(id => document.querySelector(`#${id}`));
fixture('Example page')
    .page('http://devexpress.github.io/testcafe/example');

test('Type the developer name, obtain the header text and check it', async t => {
    await t
        .typeText('#developer-name', 'John Smith')
        .click('#submit-button');

    const articleHeader = await getElementById('article-header');
    const headerText = articleHeader.innerText;

    assert.equal(headerText, 'Thank you, John!');
});

テストコードの実装はブラウザとディレクトリを指定するだけで、他に設定しなくても動きます。

$ testcafe chrome test/

テストの失敗結果が分かりやすいのも結構いいところです。

result of testcafe

Selector

先ほどのテストコードでも出てきていますが、TestCafeはSelectorsという仕組みでDOMを指定します。

DOMのとり方はブラウザで使うdocument.querySelectorなどそのままのAPIです。 注意点として取ってきた値はsnapshotであるため、あとで値が変わったときに自動的にsnapshotのnodeの値は代わりません。

import { Selector } from 'testcafe';

// A selector is created from a regular function.
// This selector will take the 'id' parameter and return
// a DOM element that has this ID.
const getElementById = Selector(id => document.getElementById(id));
test('My Test', async t => {
    const snapShotNode = getElementById('app');
});

ClickやTypeなどの操作のエミュレータはActionsという特殊なメソッドを使いますが、DOMの取得についてはSelectorsでラップすれば基本的にそのまま使えるような感じになっています。

この辺も覚えることをできるだけ減らそうとしてる感じに見えます。

リモートで実行

先ほどのテストは、ローカルにあるブラウザじゃなくても動かす事ができます。

次のようにremote:数をブラウザの欄に指定すると、connect用のURLが表示されます。 このURLにアクセスすると、そのブラウザ上で先ほどの自動テストが実行されます。

✈ $(npm bin)/testcafe remote:1 test
Connecting 1 remote browser(s)...
Navigate to the appropriate URL from each of the remote browsers.
Browser #1: http://192.168.10.3:51822/browser/connect/SJuk2E9kg

これがWebDriverではない理由の一つとして挙げられていたものですね。

また、実行できるブラウザはBrowser Provider Pluginで拡張できるようになっていて、PhantomJSとかはデフォルトでは入っていないようです。

プラグインの中身を見るとSelenium/WebDriverではないことがよくわかります。

もう少し複雑なケース

もう少し実際のテストに近いもので試してみます。

以前、ReduxのExample: Todo Listを写経したプロジェクトにE2Eテストを追加してみます。

追加したPRは以下にあります。

test controller

TestCafeのテスト構造についてはTest Code Structure | TestCafeで簡単に解説されています。

fixturepageはテスト名とテストするURLなので特に説明は入らない感じです。 テストケースはtestという関数に書いていく感じになります。

fixture('MyFixture')
    .page('http://example.com');

test('Test1', async t => {
    /* Test 1 Code */
});

基本的に非同期になるのでAsync functionとして書くようになっています。 自動テストで必要になるのは、ある要素をクリックしたり、あるinput要素にテキストを入れたりするエミュレート関数が必要です。

TestCafeではTest Controllerと呼ばれているものがそれで、上記のasync ttがTest Controllerのオブジェクトです。

Test Controllerにあるメソッドは以下にまとめられています。

テスト

coding-kata/redux-basic-tutorialはTodoリストを作るサンプルプロジェクトなので、 Todoを追加するE2Eテストを書いてみます。

Image of redux-basic tutorial

Page Objectパターンっぽく書いて次のような感じで書けました。

Test Controllerの操作は基本非同期で、TestCafeは自動で色々な操作待つ仕組みを持っています。

TestCafe automatically waits for page loads and XHRs to complete, as well as for DOM elements to become visible. You do not need to write custom code for that. -- https://github.com/DevExpress/testcafe#no-extra-coding

(TestCafeのサイトを見てるといたるところにAutomatic*という単語が出て来る)

const assert = require("assert");
import TodoPage from "./pages/index"
fixture('TodoList')
    .page('http://127.0.0.1:8080/');

test('Add TodoItem', async t => {
    const title = "NEW TODO";
    const todoPage = new TodoPage(t);
    // add item
    await todoPage.addTodo({title});
    const currentItems = await todoPage.getItems();
    assert(currentItems.length === 1);
    const [firstItem] = currentItems;
    assert.equal(firstItem.title, title);
});

10 -23-2016 21-37-53

awaitをちょこちょこ忘れたりしましたが、Test Controllerのメソッドのawait忘れは検知できるようです。 (自分で書いてる非同期はそうでもないので、Page Obejctに操作メソッドを持たせると良くないかも)

 TodoList
 ✖ Add TodoItem

   1) A call to an async function is not awaited. Use the "await" keyword
      before actions, assertions or chains of them to ensure that they run
      in the right sequence.

      Browser: Chrome 56.0.2899 / Mac OS X 10.11.6

          8 |        this.t = t;
          9 |    }
         10 |
         11 |    async addTodo({title}) {
         12 |        const input = await this.getInput();
       > 13 |        this.t.typeText(input, title);
         14 |        const button = await this.getButton();
         15 |        await this.t.click(button);
         16 |    }
         17 |
         18 |    async getInput() {

         at <anonymous>

E2Eテストでよくハマる表示されてるかどうかのタイミング問題も、基本的にTestCafeは表示に関するものは自動で一定時間待つようになっているみたいです。 (詳しい仕組みは調べてないけど、表示されてないことをテストすると時間がかかる気がする…)

デフォルトの自動待ち時間は結構長いですが、--selector-timeout msで待ち時間を指定できるようです。

$(npm bin)/testcafe chrome e2e --selector-timeout 100

おわり

ものすごく簡単にTestCafeについて紹介?しました。

Programming Interfaceもあったりするので、テストフレームワークというよりも、これを使ったブラウザ自動操作ツールとして使ったりすると面白いのかもしれないなーと思いました。

TestCafe自体も結構よくできていて、エラー表示や操作の要素を表示していて分かりやすいです。 また、Seleniumの設定が不要にしたいという気持ちはよく分かるので、ほんとに設定なしで動かせるのはすごいなと思いました。