Page Object PatternをベースにTestCafeでE2Eテストを作ってみた


以前のエントリでKotlinでSelenideを使ったE2Eテストを作ったときもPage Object Patternを利用して見通しの良いテストコードが書けました。TestCafeでも同様にPage Object Patternを利用することが推奨されています

今回はTestCafeでもPage Object Patternを利用してテストを書いてみました。

何をテストするか

次のサイトのログイン認証をテストします。

freshlive.tv

テスト内容としては次のとおりです。

  • 認証画面のURLを開く
  • IDとパスワードを入力する
  • ログインボタンをクリックする
  • ログインが完了するとHome画面にリダイレクトされるので正しくリダイレクトされているか
  • ログインが完了するとヘッダーモジュールにアカウント名が表示されてるので正しく表示されているか

テストコードのフォルダ構成

プロジェクトのフォルダ構成は次のように役割ごとに整理しました。

tests
├── features
├── operators
├── pages
└── support
  • featuresにはスペックコードをまとめます。
  • operatorsにはPageオブジェクトから画面要素を参照してログインボタンをクリックするなどのオペレーションをまとめます。
  • pagesには画面の要素を参照できるようなPageオブジェクトを画面ごとにまとめます。今回は認証画面とHome画面をPageクラスにしました。
  • supportにはスペックコードでテストを進めるときに画面キャプチャを撮るなどのユーティリティ機能をまとめます。

次からはTestCafeのAPIを利用して記述したコード例を紹介します。

Pageクラス

TestCafeのAPIを利用して認証画面のPageクラスを作ります。

import { Selector } from 'testcafe';

export default class AuthPage {
    constructor () {

        this.url = 'https://freshlive.tv/auth/fresh_id';

        this.idInput = Selector('#user_id');
        this.pwInput = Selector('#password');

        this.submitBtn = Selector('button[type=submit]');
    }
}
  • 認証画面のURLを変数に定義する。
  • テストに必要な画面要素はSelectorを利用して定義する。
  • idInputはログインID、pwInputはパスワードの入力フィールドです。
  • submitBtnはログインボタンです。

これで認証画面の要素をまとめたAuthPageクラスができあがりました。

Operatorクラス

TestCafeのAPIを利用してOperatorクラスを作ります。
今回は認証画面でログイン認証する必要があるのでログイン操作をまとめます。

import {t} from 'testcafe';
import AuthPage from '../../pages/auth/auth-page.js';

export default class AuthOperator {
    constructor () {
        this.page = new AuthPage();
    }

    async open() {
        await t
            .navigateTo(this.page.url)
    }

    async authorize(id, password) {
        await this.open();

        await t
            .typeText(this.page.idInput, id)
            .typeText(this.page.pwInput, password)
            .click(this.page.submitBtn)
    }
}
  • constructor ()でAuthPageクラスからPageオブジェクトを生成します。
  • authorize(id, password)では認証画面を開きIDの入力とパスワードの入力を行い最後にログインボタンをクリックしています。typeTextclickで入力とクリックの操作ができます。
  • tはTestCafeのTestConrollerオブジェクトです。async-awaitとimportをすることでテスト実行時のTestConrollerオブジェクトと同期できます。0.13.0のリリース(Using test controller outside of test code (#1166))でTestConrollerをテストコード外でも参照することができるようになりました。(いいね!)

Supportクラス

TestCafeのAPIを利用してのSupportクラスを作ります。
今回は画面キャプチャを撮るユーティリティクラスを作りました。

import {t} from 'testcafe';

export default class ScreenshotSupport {
    constructor () {
    }

    async take() {
        await t
            .takeScreenshot;
    }
}
  • TestControllerには画面キャプチャを撮るメソッドが用意されています。
  • 取った画像を配置するフォルダもテスト実行時のオプションで指定できます。
  • 保存される画像のサイズは表示したブラウザのサイズになり縦長の画面の場合には画面全体を撮ることはできないようです。

Specクラス

TestCafeのAPIを利用してのSpecクラスを作ります。
このクラスでテスト内容をまとめていきます。

import { expect } from 'chai';

import HomePage from '../../pages/home/home-page.js';
import AuthOperator from '../../operators/auth/auth-operator.js';
import ScreenshotSupport from '../../support/screenshot-support.js';

const homePage = new HomePage();
const authOperator = new AuthOperator();
const screenshot = new ScreenshotSupport();

const userId = ' xxxxxxxx';
const password = 'xxxxxxxx';
const accountId = 'My Account';

fixture `auth fixtures`;

test('authorized at page then is appeared your account name on element of header.', async t => {

    // ログインする
    await authOperator.authorize(userId, password);

    // スクリーンショット撮る
    await screenshot.take();

    // ログイン後にHomeにリダイレクトをするのでURLが正しく切り替わっているか
    const docURI = await t.eval(() => document.documentURI);
    expect(docURI).eql(`${homePage.url}?&login_succeeded=true`);

    // ヘッダーメニューにログインしたアカウント名が表示されているか
    await t
        .expect(homePage.accountMenuDropdown.exists).eql(true)
        .expect(homePage.accountName.exists).eql(true)
        .expect(homePage.accountName.innerText).eql(accountId);
});
  • fixtureはテストをカテゴライズする機能でテスト実行時にfixtures単位でテストを実行するオプションがあります。
  • testではテストのタイトルとテストコードを記述していきます。
  • Operatorクラスでまとめた認証画面でログインするauthorize(userId, password)を呼びだしてログインを実行しています。
  • ログイン後にURLが切り替わっているかdocURI変数にURLを入れてチェックしています。
  • 最後にexpectを利用してヘッダーモジュールにアカウント名が表示されているかをチェックしています。

まとめ

  • Page Object Design PatternベースにTestCafeでテストを書いてみました。新しいAPIということもあり画面をオブジェクト化しやすい機能が揃っている印象です。
  • テストコードから画面要素と画面操作を分離してコード化することができました。
  • 以前のエントリではChatOpsとの連携も試してみました。E2Eテストを快適に実施と運用していくことを考えるとTestCafeの採用を検討していきたい:coffee:

ソースを公開しています

github.com

関連エントリ

naruto-io.hatenablog.com

WebDriver不要のTestCafeを使ったE2EテストをChatOpsに導入してみた

今回はE2EテストをつくれるTestCafeをつかってみたエントリです。

devexpress.github.io

以前のエントリではSelenideをつかったE2Eテストの紹介をしました。Selenideも特徴がありますがTestCafeも抜群の特徴があります。
TestCafeではテストコードはNodeで書いていきます。そのため広くエンジニアメンバーがE2Eテストを書けるメリットがあります。ES2016 using をつかってテストコードが書かれているためasync/awaitなどの特徴的な構文も使えます。

最大の特徴はテストをリモートで実行できること

TestCafeのプリインストールでテストするブラウザとしてchromesafariが用意されています。ブラウザを指定したテストの実行は次のようになります。

f:id:n_soushi:20170224192652p:plain

実行するとchromesafariが立ち上がり`tests/test.js`のテストが実行されます。
この方法は予めブラウザを指定する方法ですが テストを実行するURLを生成するremoteというオプションがあります。
実行は次のようになります。

f:id:n_soushi:20170224192938p:plain

remote:1としている箇所がリモートで確認するURLの生成数を指定しています。TestCafeは指定されたURL数を生成して返します。テスターはURLをコピーして好きなブラウザにアクセスしてテストを実行できます。ブラウザにアクセスした後の実行結果は次のようになります。

f:id:n_soushi:20170224192759p:plain

TestCafeは自身でテスト用のブラウザを持たずユニークなURLを生成してテスターにブラウザを選ばせることで多種多様なブラウザのテストを可能にします。

このリモート機能を活用してE2EテストをChatOpsに組み込むアイデアを考え実装してみました。

TestCafeのRemoteConnetionをChatOpsに導入する

E2Eテストの運用にChatOpsを導入します。テスターはslackからBot経由でテスト開始を通知してBotとTestCafeが連携します。
それぞれ図にすると次のようになります。

リモートURLをslackで受け取るまで

f:id:n_soushi:20170224170519p:plain

  1. テスターはslackに@bot test web送信する。
  2. Botはイベントを受け取りTestCafeにリモートURLの生成依頼を通知する。
  3. TestCafe(hapi)は生成依頼を受け取りリモートURLを生成してBotへ返す。
  4. BotはリモートURLをルームに送信する。

こんな感じの流れで補足として次のような内容があります。

  • docker-composeでHubotとTestCafeのサビースをつくりコンテナ化した。
  • HubotとTestCafeはHTTP通信でリモートURLのやり取りを行う。
  • TestCafeにはHubotのようなwebhookはないのでhapi.jsで簡易的なAPIを作成した。
  • リモートURLの生成はドキュメントにもあるように簡単に行える。
createTestCafe('localhost', 1337, 1338)
    .then(testcafe => {
        runner = testcafe.createRunner();
        return testcafe.createBrowserConnection();
    })
    .then(remoteConnection => {

        const url = remoteConnection.url
        // ここでslackにリモートURLを送信する
        
        remoteConnection.once('ready', () => {
            runner
                .src('tests/test.js')
                .browsers(remoteConnection)
                .reporter('custom-reporter')
                .run()
                .then(function () { /* ... */ })
                .then(failedCount => { /* ... */ })
                .catch(error => { /* ... */})
        });
    });

※ 抜粋したコードなので詳細はgithubで確認できます

リモートURLを好きなブラウザで開きテスト結果を受け取るまで

f:id:n_soushi:20170224180736p:plain

  1. テスターは好きなブラウザでリモートURLを開く。
  2. TestCafeはリモートURLのアクセスを検知してテストを実行する/テスト結果をHubotに送信する。
  3. Hubotは受け取ったテスト結果をルームに送信する。

こんな感じの流れで補足として次のような内容があります。

  • TestCafeにはReporterクラスをカスタマイズすることでテスト結果のフォーマットを拡張することができる
  • コンソールにテスト結果を表示するのではなくHubotのwebhookに送信したいのでRerporterクラスを拡張した

実際のslackでBotとやり取りしている画面キャプチャ

f:id:n_soushi:20170224183659p:plain

テスターはslackをコントローラーにしてE2Eテストを開始から完了(レポート受信)までワンストップで行えます。

まとめ

  • 当初のTestCafeのリモート機能を利用したE2EテストをChatOpsに組み込むアイデアを実現できた。
  • slackと連動してattachmentをつかってレポート通知をリッチな表示にできた。
  • レポート内容は他にもあるので実運用に必要な情報の精査が必要。
  • TestCafeでテストを書いてみた、というテーマには触れていないので次回はPageModelをベースにTestCafeでE2Eテストを書いてみる。

ソースを公開しています

github.com

nginx-rtmp-module + FFmpeg + HLSで動画配信ができるdocker-composeをつくった

タイトルのdocker-composeをつくっていきます。ローカルで配信確認したいときにシュッと起動できるようにします。
次のような構成でつくりました。

コンテナ構成

f:id:n_soushi:20170217102235p:plain

それぞれのコンテナについてまとめます。

RTMP server

  • nginx-rtmp-moduleをつかってRTMP serverをたてる。

github.com

  • RTMP serverは`rtmp context`でストリームを受け取るapplicationを追加(`application encoder`)。
  • `application encoder`はストリームを受け取るとFFmpegで動画を3つのビットレードにエンコードする。
  • `application encoder`でエンコードした動画をHLS変換する`application hls`へストリームする。
  • `application hls`は変換したm3u8、tsファイルを`/data/hls`に配置する。
  • `/data/hls`はホストで同期されていてStreaming serverからもm3u8、tsファイルを参照できるようにしている。
rtmp {
    server {
        listen 1935;

        application encoder {
            live on;

            exec ffmpeg -i rtmp://localhost:1935/$app/$name
                        -c:a aac -strict -2 -b:a 32k -c:v libx264 -x264opts bitrate=128:vbv-maxrate=128:vbv-bufsize=128 -rtbufsize 100M -bufsize 256k -preset veryfast -f flv rtmp://localhost:1935/hls/${name}_low
                        -c:a aac -strict -2 -b:a 64k -c:v libx264 -x264opts bitrate=256:vbv-maxrate=256:vbv-bufsize=256 -rtbufsize 100M -bufsize 512k -preset veryfast -f flv rtmp://localhost:1935/hls/${name}_mid
                        -c:a aac -strict -2 -b:a 128k -c:v libx264 -x264opts bitrate=512:vbv-maxrate=512:vbv-bufsize=512 -rtbufsize 100M -bufsize 1024k -preset veryfast -f flv rtmp://localhost:1935/hls/${name}_high;
        }

        application hls {
            live on;

            hls on;
            hls_path /data/hls;
            hls_nested on;
            hls_fragment 2s;

            hls_variant _low BANDWIDTH=160000;
            hls_variant _mid BANDWIDTH=320000;
            hls_variant _high  BANDWIDTH=640000;
        }
    }
}

Broadcast server

  • mp4ファイルをFFmpegを使いループ再生させRTMP serverへストリームしている。
  • Broadcast serverはコンテナの1つのためlocalhostではRTMP serverに接続できない。
  • 次のように環境変数を参照するようにしている。
ffmpeg -re -stream_loop -1 -i '/data/broadcast_source.mp4' -f flv "${RTMP_SERVER_URL}/${STREAM_NAME}"
  • docker-composeで環境変数 'RTMP_SERVER_URL'と'STREAM_NAME'を定義している。

Streaming server

  • NginxでHTTP serverをたてる。
  • typesに'm3u8'と'ts'を追加している。
  • rootを'/data'としてRTMP serverと同期しているm3u8とtsファイルにアクセスしている。
server {
        listen 80;

        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }
            root /data;
            add_header Cache-Control no-cache;
            add_header Access-Control-Allow-Origin *;
        }
    }
}

docker-compose

ホストとコンテナでファイルを同期しているのでdindを使いホストもコンテナ化しています。

ホスト側のdocker-compose

version: "3"

services:
  dind_streaming_host:
    container_name: streaming_host
    image: docker:1.13.0-dind
    privileged: true
    ports:
      - 5301:2375
      - 1935:1935
      - 80:80
      - 5000:5000
    volumes:
      - ./video/clouds_over_the_mountain_hd_stock_video.mp4.mp4:/var/container/data/broadcast_source.mp4

コンテナに必要なポートと`Broadcast server`から配信する動画をコンテナに同期しています。

コンテナ群のdocker-compose

version: "3"

services:
  rtmp:
    container_name: rtmp_server
    build: ./container/nginx_rtmp
    ports:
      - 1935:1935
    volumes:
      - /var/container/data:/data
      - /var/container/log/rtmp/nginx/:/var/log/nginx

  stream:
    container_name: stream_server
    build: ./container/nginx_stream
    ports:
      - 80:80
    volumes:
      - /var/container/data:/data
      - /var/container/log/stream/nginx/:/var/log/nginx

  broadcast:
    container_name: broadcast_server
    build: ./container/ffmpeg_broadcast
    depends_on:
      - rtmp
    environment:
      RTMP_SERVER_URL: "rtmp://rtmp:1935/encoder"
      STREAM_NAME: broadcast
    volumes:
      - /var/container/data/:/data

起動方法と動画配信方法

ホストとコンテナでファイルを同期しているのでdindを使いホストもコンテナ化しているので起動にはdindとdirenvが必要です。

docker-composeの起動方法

まずはホスト側の起動から。
もしdindのイメージがなければプルします。

docker pull docker:1.13.0-dind

ホスト側を起動します。

$ cd (path-to 'streaming-host-sample')
$ docker-compose up -d

続いてコンテナ群をまとめているdocker-composeを起動します。

$ cd (path-to 'streaming_host')
direnv: loading .envrc
direnv: export +DOCKER_HOST
$ docker-compose up -d

※ 'streaming_host'ディレクトではdirenvによってディレクトリでは環境変数`DOCKER_HOST`が先に起動したdockerのホストに切り替わっています。

これですべてのコンテナが起動できました。

動画の確認方法

コンテナが起動できたらsafariで`http://localhost/hls/broadcast.m3u8`にアクセスすると`Broadcast server`から配信している動画を再生できます。
今回はプレイヤーを用意せずsafariで直接m3u8を参照しています。

f:id:n_soushi:20170217113815p:plain

サンプルの動画はDISTILLからDLしました。

www.wedistill.io

ライブ配信方法

wirecastなどの配信ツールを使うことでライブ配信ができます。

www.telestream.net

出力先のRTMPサーバに'rtmp://localhost:1935/encoder'を指定します。
ストリームを'live'として配信した場合は`http://localhost/hls/live.m3u8`にアクセスするとライブ配信した動画を確認できます。

まとめ

ソースを公開しています

github.com