Tech Racho エンジニアの「?」を「!」に。
  • 開発

Docker + Puppeteer を使ったテスト自動化の入門体験記

ebi です。前回の記事と同タイミングに下書きしていた記事なのに何故か半年間放置されていました……。
いつかやろうと思っていた Puppeteer 記事について、ウイングドアさんの記事に先越されちゃったので、Puppeteer の基本的な説明なんかはそちらに任せていきなり本題から入っていきます。

なお、この記事はもっと雑にやろうとしたことやったことを垂れ流していくスタンスの記事で落差が酷いですが予めご了承ください。
特に目新しいツールの記事でもなくて恐縮ですが、過去の記事の傾向からして何をいまさらなので気後れせずやっていきます。

前置き

  • この記事での自動化させたいテストとは、普段実際にブラウザを使用して画面を操作して期待する画面の表示、動き、ページ遷移をテストするような動作確認の類です。
  • 画面操作と期待する結果について、厳密なテストコード化はせず、スクリーンショットを撮影して終わります。
  • リグレッションテストもこの記事内では取り扱いません。
  • 弊社内プロジェクトで、もっと高度に Puppeteer を使いこなしている実績もあるはずですが、その域に至っていないのでお試しで触ってみた内容の記事になります。
  • 触りのインストールと簡単な動作確認だけして終わりのような記事ではなく、なるべく実践的な最初の環境構築、実例を備えた記事の執筆を目指しています。
  • 今回も最終的な成果物を GitHub に置いておきます。かいつまんで記事にしているので、全体的なサンプルコード例はこっちで見てください。

Docker を使って Puppeteer 動かそう

nodejs を Windows に入れるの面倒だなと思ったので Docker でやります。懲りずに今回も僕は Windows 環境で docker-machine でやっています( 動作確認のため、 Ubuntu 16.04 でもやってみて少しずつ調整した内容も含まれています )。

信じるべきものは 公式サポート です。バッチリ Docker を使った例があるので、これを見ながら初歩の初歩から始めていきます。

  • 例と同じ Dockerfile を用意して、 docker build -t puppeteer-chrome-linux . します。
  • 続いて、コンテナ起動のコマンドですが、 Windows を使っているので cat の代わりに type も使ってみたけど上手くいかなかったので、コンテナ内で作業する形式にしました。
docker run -it --rm --cap-add=SYS_ADMIN -v /c/Users/ebi/Desktop/techracho-puppeteer:/work puppeteer-chrome-linux /bin/bash

と唱えます(マウントするディレクトリの指定は各自で調整してください)。

  • 無事コンテナ内に潜り込めたらあとは work ディレクトリに行って作業します
  • 一番最初の例を真似します。
    ファイル名を capture.js として、 URL だけ有効なものにしました。
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.bpsinc.jp/');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();
  • node capture.js で実行します。何も指定してないので、以下のように特にメッセージ出て来ずに終わりますが気付けば example.png ができているはずです。

コード中に console.log() を書くと、随時ターミナル上にメッセージが出てきます。

pptruser@b751253d5656:/work$ node capture.js
pptruser@b751253d5656:/work$

こんな感じのものが保存されました。まだ読み込み中でしたかね?
コード例のすぐ下に書いてありますが、デフォルトだと 800×600 の画面幅サイズで画像を撮るので Page.setViewport() で調整してくれ、とのこと。

docker-compose にして、同じネットワーク内のページを表示する

本番環境を直接確認する場合は Docker 単体でいいのですが、開発環境下での動作確認や CI を回すうえでは docker-compose 構成にしてしまうかな、と思うので。
そんなわけで、手頃な docker-compose 環境が欲しいので、前回のメール Docker イメージ利用記事で使った docker-compose 環境を応用します。

不要な指定も含まれていそうですが晒します。追加で docker-compose の例を見たくて この記事 を参考にしましたが、元の Dockerfile の指定 CMD ["google-chrome-unstable"] が実行エラーになったりするので、その辺は諦めました( Gtk-WARNING **: cannot open display: と出るのでモニター周りの設定が上手くいかないらしいけど解決できなかった)。

前回記事からの Wen サーバのコンテナ定義更新差分

以前まで、 Web サーバ用のコンテナに ssmtp をインストールしていたのですが、 ssmtp は2019年12月現在メンテナンスが止まったままのようで、推奨されている msmtp を使用するように更新しました。

  • Dockerfile
FROM php:7.3-apache

RUN apt-get update; apt-get install -y msmtp
COPY msmtprc /etc/msmtprc

最低限の設定ファイルが必要だったので、こんな感じのものを用意して Web サーバ用コンテナの /etc/msmtprc に配置しておくと動くはずです。

  • msmtprc
host mail
from "postmaster@example.com"
  • php.ini
sendmail_path = "/usr/bin/msmtp -t"

docker-compose について

  • docker-compose.yml
version: '3'
services:
  web:
    build: .
    volumes:
      - ./web_root:/var/www/html # リポジトリごちゃついてるんで、今回はドキュメントルート内に含めたいものは web_root 内にしてます
    ports:
      - "3080:80"

  mail:
    image: djfarrelly/maildev
    ports:
      - "1080:80"

  puppeteer:
    build: ./puppeteer
    tty: true
    stdin_open: true
    cap_add:
      - SYS_ADMIN
    volumes:
      - ./puppeteer:/home/pptruser/work
    command: /bin/bash -c "cd /home/pptruser/work && node capture.js" # 立ち上げ時に自動でキャプチャー取得しておく

command は適当に。 puppeteer コンテナは指定の JS の処理が終わったら Exit してしまうので command が /bin/bash 等になってると、とりあえずデタッチドモードで起動した時に起動したままにできるとは思います。
最終的に docker-compose 化した恩恵を受けているんだかいないんだか、謎にはなりましたが深く気にするのはやめます。

前回記事通り、同じネットワーク内ではホスト名を指定できるので URL を http://web って感じで始めると、 Puupeteer を動かしているコンテナから上手く繋がりそうです。
利便性を考慮してスクリーンショットした画像のファイル名とかディレクトリを調整したりした結果、実行ファイルはこんな感じにしました。

  • capture.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.setViewport({width: 1024, height: 800})
  const url = 'http://web/techracho-sample.html';
  let now = new Date();
  now.setHours(now.getHours() + 9);

  await page.goto(url);
  await page.screenshot({path: 'logs/capture_' + dateToFormat(now) + '.png'});
  console.log("save screenshot: " + url);
  await browser.close();
})();

function dateToFormat(date) {
  const year     = date.getFullYear();
  const month    = date.getMonth() + 1;
  const day      = date.getDate();
  const hour     = date.getHours();
  const min      = date.getMinutes();
  return `${year}${month}${day}_${hour}_${min}`;
}

HTML の指定が雑過ぎて文字化けしてる……

ちゃんと HTML に utf-8 の文字コード指定したら、見れるようになりました(またあとで掲載するので割愛)。

Puppeteer を使って、画面操作をしてみます

丁度良い環境が整ってきたので、 puppeteer を使ってのメール送信ボタンの操作と画面遷移の確認。メールの増減を確認してみます。

基本動作に必要な API の確認

  • 入力欄への記入操作page.type(selector, text[, options])
    await page.type('input[name="name"]', 'hogehoge'); のように、セレクタ、入れたい文字を指定します。分かりやすい。
  • クリック動作page.click(selector[, options])
    これも await page.click('input[type="submit"]'); のように、セレクタ指定するだけ。
    上記では送信ボタンのクリックをしていますが、マウスクリックではなく Enter を押す操作でのフォーム送信とかのテストもできるよう。
  • スマホ端末での閲覧をエミュレートするpage.emulate(options)
    何を隠そう 2019 年 12 月現在の Techracho がそうなんですが、スマホ端末でアクセスした時とそうでない時とでサーバ側から出力される内容が違う場合があります。
    もちろん単に想定端末での画面サイズ指定として使っても良いと思います。このファイルからエミュレートしたい端末が選び放題です。

最終的にできあがったもの

  • capture.js
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.setViewport({width: 1024, height: 800});
  const url = 'http://web/techracho-sample.html';
  const mailUrl = 'http://mail';
  let now = new Date();
  now.setHours(now.getHours() + 9);

  await page.goto(url); // 1. 入力画面への入場
  await page.screenshot({path: 'logs/capture_before_' + dateToFormat(now) + '.png'});
  console.log("save screenshot: " + url);

  await page.type('input[name="name"]', 'hogehoge'); // 2. フォーム内の任意の記入欄への入力を行う
  await page.screenshot({path: 'logs/capture_after_' + dateToFormat(now) + '.png'});
  console.log("save screenshot after input: " + url);

  await page.click('input[type="submit"]'); // 3. フォームの送信を行って、メールが送信されたことを確認する
  await page.goto(mailUrl);
  await page.click('a.email-item:first-of-type');
  await page.screenshot({path: 'logs/capture_mail_' + dateToFormat(now) + '.png'});
  console.log("save screenshot maildev");

  const iPhone = puppeteer.devices['iPhone 6'];
  const techrachoUrl = 'https://techracho.bpsinc.jp/';
  await page.emulate(iPhone);
  await page.goto(techrachoUrl); // おまけ: スマホ端末をエミュレートして、 Techracho の SP 版サイトのスクリーンショットを撮影する
  await page.screenshot({path: 'logs/capture_techracho_' + dateToFormat(now) + '.png'});
  console.log("save screenshot Techracho for SP");

  await browser.close();
})();

function dateToFormat(date) {
  const year     = date.getFullYear();
  const month    = date.getMonth() + 1;
  const day      = date.getDate();
  const hour     = date.getHours();
  const min      = date.getMinutes();
  return `${year}${month}${day}_${hour}_${min}`;
}

1. 入力画面への入場

2. フォーム内の任意の記入欄への入力を行う

3. フォームの送信を行って、メールが送信されたことを確認する

おまけ: スマホ端末をエミュレートして、 Techracho の SP 版サイトのスクリーンショットを撮影する

おわり

エンジニアたるもの便利そうなツールを積極的に実践、導入したい気持ちはありつつ、まともに触る機会がないまま引きずっていてこの記事を書きながら触りました。
実際のサイトでテストするにあたっては、もっと記述量とか操作が増えるので大変そうですが、基本的には JS 書けば動くので頑張ればやれそうな見通しは付きました。

前置きで断った通り、色々と手出していないこともまだまだ沢山あるので、その他のフロントエンドライブラリと組み合わせて使いこなせるとさらに夢が広がりますね。

関連記事

Docker イメージを利用したローカル開発環境向けメールサーバ構築のすゝめ

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。