見出し画像

Github ActionsとSelenoidでファイルダウンロードや録画をやってみたい

こんにちは、テスト自動化アーキテクトの森川です。

みなさん自動テストされていますか?

突然ですが、私はE2E自動テストをまわしていくうえで欠かせない要素は3つあると思っています。

・よりスケーラブルに
・よりステーブルに
・よりサスティナブルに

本日はひとつめの「スケーラブル」について
Seleniumを軽量・高速にスケールしてくれる技術「Selenoid」を使ったエントリーをお届けしましょう。

具体的には、GitHub ActionsにSelenoid環境を構築してテスト実行時のビデオ録画とファイルダウンロードにトライしてみます。

本エントリーでやってみること

・GitHub ActionsでSelenoidテスト環境構築
・Seleniumによるファイルダウンロードテスト
・実行時のビデオとログをレポートに添付

これらを1つのクラウド・インスタンス上で実行します。


『全然スケールしてないじゃないか』というお叱りの声が聞こえそうですね。

はい。すみません。でも、まずはSelenoidがE2Eテストのユースケースに耐えられるかを見極めさせていただきたいと思います。

SelenoidのIaCな環境構築については当ブログのこちらのエントリーを御覧ください。

【IaCで作るselenium環境】01_selenoidを構築する

リモートテスト環境の課題

SelenoidはSelenium Grid的なリモートテスト環境をスケールする場合に非常に優れたライブラリですが、リモートゆえの弱点もあります。

画像1

たとえば、OSのダイアログ操作はできません。ブラウザ操作でOSのダイアログが開いたらそこから先はテストが進められなくなります。

また、ファイルダウンロードにひと手間が必要です。そしてリモートサーバ内のコンテナ上で実行されているため、トラブル時の原因調査が難しくなりがちです。

過去の自動テスト案件でもこういった弱点のために、Selenium Grid(Selenoid)なテスト環境を諦めたことが何度かありました。対案としてはCIサーバとCI Agentでスケールしたり、ということになりますがこれはこれで別の難点があります。

画像2

Selenoidにはファイルダウンロードやビデオ録画のAPIが用意されています。これらをうまく使えば弱点を回避できるのでは?と思いサンプルテストコードを書いてみました。

ソースコードはすべて公開しています。https://github.com/tmshft/SelenoidTest

検証環境

・言語:Java:AdoptOpenJdk11
・ビルドテスト実行:Gradle:6.8.3
・Actionsランナー:Ubuntu latest(20.04)
・テストFW:Selenium beta-3
・Selenoid 1.10.1
・レポーティング: Allure Framework

実行結果

いきなりですが実行結果です。
https://tmshft.github.io/SelenoidTest

画像7

失敗しているのは、OSのダイアログが表示されるためにエラーとなるケース(敢えてやってみたかったです。)と、クリック対象が存在しないためにエラーとなるケースです。(こちらはSelenoidのブラウザログにエラーが出力されることを確認するためです)

レポートにはダウンロードされたファイルやビデオなどが添付されています。

画像5

ファイルダウンロードテストの成功時ビデオはこちらです。

画像3

しっかりダウンロードされていますね。

次は失敗ケースです。ダウンロード時のダイアログが表示されています。(ブラウザだけでなくデスクトップ全体を撮っているのが萌えますね!)

画像7

余談ですがSelenoidをGoogle検索していると「もしかしてSelenide?」とサジェストされることが多いのが辛いです。SelenideはSeleniumのラッパーライブラリです。SelenoidとSelenide、確かに似ていますが北海道とサッポロ一番ぐらい似て異なるものなんですよね。

Actionsパイプライン

GitHub Actionsのパイプラインはこちらです。

.github/workflows/pipeline.yml
name: Selenoid Test Example
on:
 pull_request:
   branches:
     - "*"
jobs:
 build:
   name: Selenoid Test
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v1
       name: checkout

     - name: run-selenoid
       run: |
         docker-compose up -d

     - name: execute test
       run: |
         chmod +x ./gradlew
         ./gradlew -Dselenoid.base.url=http://127.0.0.1:4444 -Dselenoid.path=/wd/hub \
         clean test --tests ExampleTest
         hostname=`echo "$GITHUB_REPOSITORY" | sed -e "s/\//.github.io\//g"`
         echo "::set-output name=HOSTNAME::${hostname}"
       id: execute-test

     - name: get report history
       uses: actions/checkout@v2
       with:
         ref: test-report
         path: test-report

     - name: create report with history
       uses: simple-elf/allure-report-action@master
       with:
         allure_results: build/allure-results
         gh_pages: test-report
         allure_report: build/allure-report
         allure_history: build/allure-history

     - name: deploy to pages
       uses: peaceiris/actions-gh-pages@v3
       with:
         github_token: ${{ secrets.GITHUB_TOKEN }}
         publish_dir: build/allure-history
         publish_branch: test-report

     - name: comment to pr
       uses: actions/github-script@0.3.0
       if: github.event_name == 'pull_request'
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
           const { issue: { number: issue_number }, repo: { owner, repo }  } = context;
           hostname=`echo "$GITHUB_REPOSITORY" | sed -e "s/\//.github.io\//g"`
           github.issues.createComment({ issue_number, owner, repo, body: "ci-test report 👋 <br/><a href=https://${{ steps.execute-test.outputs.HOSTNAME }}/>see report</a>" });

かいつまんで見ていきましょう。

run-selenoid
docker-composeでSelenoidをデプロイ・起動しています。

execute test
Gradleでテストコードのビルドとテスト実行をしています。
VMoptionのselenoid.base.url、selenoid.pathはローカル/クラウド実行を切り替えるためのインターフェースとして使っています。

get report history
ブランチtest-reportにコミットしたレポート資産をいったんチェックアウトしています。履歴を含んだレポートページを作成するためです。

create report with history
履歴込みでテスト結果レポートを作成しています

deploy to pages
ブランチtest-reportの内容をGitHub Pagesにデプロイします。Allure RerportのPages化については、以前のエントリーで詳しく書いています。

Selenoidの起動

Selenoidの起動ははdocker-composeでひとまとめにしています。

docker-compose.yml

version: '3'
services:
 selenoid:
   network_mode: bridge
   container_name: selenoid
   image: aerokube/selenoid:1.10.1
   volumes:
     - "$PWD/config:/etc/selenoid"
     - "$PWD/config:/opt/selenoid"
     - "/var/run/docker.sock:/var/run/docker.sock"
   environment:
     - OVERRIDE_VIDEO_OUTPUT_DIR=$PWD/config/video
   command: ["-conf", "/etc/selenoid/browsers.json", "-video-output-dir", "/opt/selenoid/video", "-log-output-dir", "/opt/selenoid/logs", "-session-attempt-timeout", "180s"]
   ports:
     - "4444:4444"

 chrome-latest:
   image: selenoid/chrome:latest

 video-recorder:
   image: selenoid/video-recorder:latest-release

SelenoidのDockerイメージをPullして起動しています。あわせてselenoid/chrome、selenoid video-recorderのイメージも必要となるので、ここでPullしておきます。

ノード側の設定ファイルとしてconfig/browsers.jsonが別途必要です。

Selenoidのノード接続

SelenoidのノードをRemoteWebDriverで起動しています。

ファイルダウンロード時のダイアログ制御やダウンロード先のディレクトリをあらかじめ指定してからChromeを起動しています。特に変わったことはしておらず、いずれもSeleniumやSelenoid関連でググればすぐにでてくるような内容です。

// jp.shiftinc.automation.ExampleTest

void setUp(Boolean allowPopup) {
   videoName = String.format("%s.mp4", RandomStringUtils.randomAlphanumeric(10));
   driver = new RemoteWebDriver(nodeUrl,setChromeOption(allowPopup));
   driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
   sessionId = driver.getSessionId().toString();
}

ChromeOptions setChromeOption(Boolean allowPopup) {
   ChromeOptions options = new ChromeOptions();
   Map<String, Object> prefs = new HashMap<>();
   prefs.put("profile.default_content_settings.popups", 0);
   prefs.put("download.default_directory", "/home/selenium/Downloads");
   prefs.put("download.prompt_for_download", allowPopup);
   options.setExperimentalOption("prefs", prefs);
   return options.merge(setCapabilities());
}

DesiredCapabilities setCapabilities() {
   DesiredCapabilities capabilities = new DesiredCapabilities();
   capabilities.setBrowserName("chrome");
   capabilities.setCapability("enableVideo", true);
   capabilities.setCapability("enableVNC", true);
   capabilities.setCapability("enableLog", true);
   capabilities.setCapability("videoName", videoName);
   return capabilities;
}​

ファイルダウンロードのテスト

Seleniumのサイトでzipファイルをダウンロードするテストを書いてみました。あらかじめ用意したファイルとMD5で比較します。
@ParameterizedTestで、ダウンロード時にダイアログが表示されないパターン(=成功)と、表示されるパターン(=失敗)を展開しています。

// jp.shiftinc.automation.ExampleTest

@Story("Example for download file")
@ParameterizedTest(name = "allow popup when download => {0}")
@ValueSource(booleans = {false, true})
void canDownloadCorrectFile(boolean allowPopup) throws IOException, InterruptedException {
   String IE_ZIP = "IEDriverServer_Win32_3.150.1.zip";
   String REFER_PATH = "src/test/resources/" + IE_ZIP;

   setUp(allowPopup);

   // store original file MD5
   assertMD5(new File(REFER_PATH), false);

   // access url
   driver.get(TEST_URL);
   attachFileToReport(driver.getScreenshotAs(OutputType.FILE), "img01","image/png","png");

   // download file
   WebElement elm = driver.findElement(By.cssSelector("a[href*=\"IEDriverServer_Win32_3\"]"));
   elm.click();

   // get file by using selenoid api
   URL url = new URL(String.format("%s/download/%s/%s", baseUrl, sessionId, IE_ZIP));
   File download = downloadFile(url);
   attachFileToReport(download, "IEDriverServer", "application/octet-stream", "exe");

   // assert file by MD5
   assertMD5(download, true);
}

SelenoidのファイルダウンロードはAPI経由で行います。RemoteWebDriverのセッションIDとファイル名が必要です。

http://selenoid-host.example.com:4444/{session-id}/download/{your_file.txt}

Selenoid - A cross browser Selenium solution for Docker

このURLを与えてファイルを取ってきてくれるサービス関数を書きます。URLにセッションIDが含まれているのは並列化したときに強そうで良いですね。

// jp.shiftinc.automation.ExampleTest

File downloadFile(URL url) throws IOException, InterruptedException {
   String path = url.getPath();
   Thread.sleep(5000);
   String downloadFile = "build/tmp/" + path.substring(path.lastIndexOf("/") + 1);
   try (DataInputStream in = new DataInputStream(url.openStream());
           DataOutputStream out = new DataOutputStream(new FileOutputStream(downloadFile))) {
       byte[] buf = new byte[8192];
       int len;
       while ((len = in.read(buf)) != -1) {
           out.write(buf, 0, len);
       }
       out.flush();
   }
   return new File(downloadFile);
}

あらかじめファイル名がわかっている場合は良いですが、動的なファイル名のように把握できない場合もあります。下記のURLでいったんファイルリストを取得してからダウンロードすることなるようです。

http://selenoid-host.example.com:4444/{session-id}/download

ビデオの取得

テスト実行時のビデオもAPI経由でダウンロードします。

http://selenoid-host.example.com:4444/video/<filename>.mp4

Selenoid - A cross browser Selenium solution for Docker

ビデオファイル名はあらかじめ適当に決めておきます。こちらも並列化する際にはセッションIDなどで一意にしたほうが良さそうです。

// jp.shiftinc.automation.ExampleTest#setUp

videoName = String.format("%s.mp4", RandomStringUtils.randomAlphanumeric(10));

ダウンロードしたビデオファイルはレポートに添付します。テストが失敗した場合でも取得するように、JUnit5のTestWatcherで成功時/失敗時をフックしておきます。

class SelenoidTestWatcher implements TestWatcher {

   @Override
   public void testSuccessful(ExtensionContext context) {
       try {
           tearDown(context);
       } catch (IOException | InterruptedException e) {
           e.printStackTrace();
       }
   }
   @Override
   public void testFailed(ExtensionContext context, Throwable cause) {
       try {
           tearDown(context);
       } catch (IOException | InterruptedException e) {
           e.printStackTrace();
       }
   }

   private void tearDown(ExtensionContext context) throws IOException, InterruptedException {
       ExampleTest exampleTest = (ExampleTest)context.getRequiredTestInstance();
       exampleTest.downloadVideo();
       exampleTest.downloadLog();
   }
}

ログファイルの取得

SelenoidではNodeコンテナ内のブラウザログを取得できます。原因がわからないときに役に立ってくれそうです。
Selenoid - A cross browser Selenium solution for Docker

要素が見つからずに失敗するテストを書いてみました。ブラウザのログがエラー時にどこまで出るかを確認するためです。

// jp.shiftinc.automation.ExampleTest

@Story("Example for failed test(please check browser log)")
@Test
void cannotClickElement() throws IOException {
    setUp(true);
    driver.get(TEST_URL);
    attachFileToReport(driver.getScreenshotAs(OutputType.FILE), "img02","image/png","png");
    driver.findElement(By.cssSelector("a[href*=\"not_exists\"]")).click();
}

ビデオファイルと同様にAPI経由で取得してレポートに添付します。
結果を見るとしっかりとログにエラーが出力されていました。

画像8

browers.jsonであらかじめログの詳細(VERBOSE)を設定しておきます。(この設定はテストコード側では制御できないようです。)

# config/browers.json
  
"chrome": {
  "default": "latest",
   "versions": {
     "latest": {
       ...
       "env": ["VERBOSE=true"],
       ...
    }
  }
}

補足

本エントリーの趣旨とは少しはなれますが、レポーティングについて補足です。
GitHub PagesにAllureのテスト結果レポートを出力していますが、単純にレポートを作成するだけでは過去分は上書きされてしまいます。

リグレッションテストの運用では過去データと比較する場合も考えられますので、履歴を保持するようにActions Marketplaceからsimple-elf/allure-report-actionを利用させていただきました。

これで過去分のレポートをいつでも見られるようになりました。

まとめ

SelenoidのAPIを使用したファイルダウンロードはそれほど難しくなさそうです。ビデオ・ログファイルについても同様です。スケールした場合にテスト間でコンフリクトしないように設計されていると思います。

今回はChromeだけでのトライアルでしたが、他ブラウザへの対応は課題です。(IEへの対応はきっと難しいことでしょう)

ビデオ録画はノードコンテナのOSデスクトップを撮るので異常発生時の調査に役立ちそうです。ブラウザのレンダリング範囲しか撮れないWebDriverのスクリーンショット機能とは大きな違いを感じます。

ただし、ビデオやログファイルはストレージを圧迫することになるのでツール設計時には配慮が必要です。今回は未検証ですがSelenoidではAWSのS3バケットにデータを投げ込むAPIも用意されているようですので、こちらをうまく使えばよいのかもしれません。

次回はスケーラブルな自動テストの作成についてトライしてみたいと思います。

それではみなさん Happy Automation Testing Journey!!

__________________________________

執筆者プロフィール:森川 知雄
中堅SIerでテスト管理と業務ツール、テスト自動化ツール開発を12年経験。
SHIFTでは、GUIテストの自動化ツールRacine(ラシーヌ)の開発を担当。
GUIテストに限らず、なんでも自動化することを好むが、ルンバが掃除しているところを眺めるのは好まないタイプ。
さまざま案件で自動化、効率化によるお客様への価値創出を日々模索している。

このSHIFT公式ライターの他の記事を見る

画像4

■SHIFTのサービス一覧
品質に関するお悩みにぴったりなサービスを揃えております。


■SHIFTについて

私たちはソフトウェアテスト(第三者検証)のプロ集団です。


あなたの「スキ」が励みになります!
「無駄をなくしたスマートな社会の実現」を目指し、ソフトウェア製品の開発、運用、マーケティングなど、あらゆる立場から携わるSHIFT Groupの公式noteです。エンタメ業界から、Web系、金融/製造/小売りなどのエンタープライズ業界まで広い知見を活かした情報を発信しています。