Page Object PatternをベースにTestCafeでE2Eテストを作ってみた
以前のエントリでKotlinでSelenideを使ったE2Eテストを作ったときもPage Object Patternを利用して見通しの良いテストコードが書けました。TestCafeでも同様にPage Object Patternを利用することが推奨されています。
今回はTestCafeでもPage Object Patternを利用してテストを書いてみました。
何をテストするか
次のサイトのログイン認証をテストします。
テスト内容としては次のとおりです。
- 認証画面の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の入力とパスワードの入力を行い最後にログインボタンをクリックしています。typeTextとclickで入力とクリックの操作ができます。
- 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; } }
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を利用してヘッダーモジュールにアカウント名が表示されているかをチェックしています。
まとめ
ソースを公開しています
関連エントリ
WebDriver不要のTestCafeを使ったE2EテストをChatOpsに導入してみた
今回はE2EテストをつくれるTestCafeをつかってみたエントリです。
以前のエントリではSelenideをつかったE2Eテストの紹介をしました。Selenideも特徴がありますがTestCafeも抜群の特徴があります。
TestCafeではテストコードはNodeで書いていきます。そのため広くエンジニアメンバーがE2Eテストを書けるメリットがあります。ES2016 using をつかってテストコードが書かれているためasync/awaitなどの特徴的な構文も使えます。
最大の特徴はテストをリモートで実行できること
TestCafeのプリインストールでテストするブラウザとしてchromeとsafariが用意されています。ブラウザを指定したテストの実行は次のようになります。
実行するとchromeとsafariが立ち上がり`tests/test.js`のテストが実行されます。
この方法は予めブラウザを指定する方法ですが テストを実行するURLを生成するremoteというオプションがあります。
実行は次のようになります。
remote:1としている箇所がリモートで確認するURLの生成数を指定しています。TestCafeは指定されたURL数を生成して返します。テスターはURLをコピーして好きなブラウザにアクセスしてテストを実行できます。ブラウザにアクセスした後の実行結果は次のようになります。
TestCafeは自身でテスト用のブラウザを持たずユニークなURLを生成してテスターにブラウザを選ばせることで多種多様なブラウザのテストを可能にします。
このリモート機能を活用してE2EテストをChatOpsに組み込むアイデアを考え実装してみました。
TestCafeのRemoteConnetionをChatOpsに導入する
E2Eテストの運用にChatOpsを導入します。テスターはslackからBot経由でテスト開始を通知してBotとTestCafeが連携します。
それぞれ図にすると次のようになります。
リモートURLをslackで受け取るまで
- テスターはslackに@bot test webを送信する。
- Botはイベントを受け取りTestCafeにリモートURLの生成依頼を通知する。
- TestCafe(hapi)は生成依頼を受け取りリモートURLを生成してBotへ返す。
- 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で確認できます
まとめ
ソースを公開しています
nginx-rtmp-module + FFmpeg + HLSで動画配信ができるdocker-composeをつくった
タイトルのdocker-composeをつくっていきます。ローカルで配信確認したいときにシュッと起動できるようにします。
次のような構成でつくりました。
コンテナ構成
それぞれのコンテナについてまとめます。
RTMP server
- 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}"
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を参照しています。
サンプルの動画はDISTILLからDLしました。