slack から EC サイトの自動注文テストを走らせる (puppeteer)

# 概要

## お題

[EC-CUBE3 (オープンソースの EC サイト)](https://www.ec-cube.net/product/info_3.php) の各決済の注文完了までのテストを作ってみます
　
もしよろしければ下記も御覧ください
[AWS EC2 に EC-CUBE 3 をインストールした時のメモ](https://qiita.com/sunadorinekop/items/70cdd4efcedb2ad95dc2)

## ゴール

test と発言すると E2E テストが走り、結果を表示してもらうようにします

<img width="765" alt="goal.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/2a0e6579-9536-4194-d7b9-e22aff8632c1.jpeg">

## サンプルのソース

GitHub に up していますので良かったらどうぞ！

- https://github.com/sunadorinekop/puppeteer-hubot-mocha-sample

## 利用するもの

- puppeteer
- heroku
- slack
- hubot
- mocha
- nodejs

## やろうと思ったきっかけ

現在 Jenkins + AWS + Selenium + docker の自動テストを利用していますが・・

- インフラ絡みで問題が起きた時に解決に時間がかかることがある
- 1 週間に 4 - 5 時間程度の稼働なので、常時 EC2 を立てておくのがもったいないかも
- Jenkins は細かく設定できる反面、作り込まれた job は他の人がメンテナンスしにくくなりがち
- 利用者視点だと実はテスト成功 / 失敗がすぐわかって、失敗した場合はどこで失敗したのかがちょっとわかる程度で十分かも
- メンテナンスしたいけどローカルの環境構築に時間がかかり、そこで諦めてしまう人も:cry:

↓

- インフラの問題をサーバーレスで解決
- 料金をサーバーレスで節約
- 設定をコードに集約させることで、コードだけ見ればわかるようにし、作った人以外でもメンテナンスしやすくする
- 更に Node.js だと Windows でもローカルの環境構築がサクっとできる気がするのでメンテナーが増える (といいなあ:smile:)

## puppeteer とは

- [puppeteer](https://github.com/GoogleChrome/puppeteer) (パペッティア) は公式の　GoogleChrome のチームが開発している Node.js ライブラリです
- スクレイピング等がサクっと行えます
- phantomJS から乗り換える人が多くなってきた印象です

# 手順

## 1. heroku で Create new app する

<img width="1431" alt="heroku.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/6f951050-f71e-4951-3842-f37c076ec8ab.jpeg">

puppeteer-hubot-mocha-sample という名前で作ってみました

<img width="1430" alt="Screen Shot 2018-03-18 at 14.40.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/106789/a2fe0801-e90a-e015-d050-8c5ad581d8ad.png">


## 2. とりあえず hubot をデプロイ

~~~bash
# ローカルリポジトリを作成
$ mkdir puppeteer-hubot-mocha-sample
$ cd !$
$ git init
$ heroku git:remote -a puppeteer-hubot-mocha-sample

# 必要なパッケージをインストール
$ npm install -g yo generator-hubot
$ npm install -S coffee-script

# いくつか質問に答えながらインストール
$ yo hubot

# Bot adapter は Third-party adapater の slack を選択することを忘れずに！
# ? Bot adapter slack
~~~

async/await を使いたいので、7.6 以降にします
ここでは現在 LTS 推奨版の 8.10.0 にします

~~~bash
$ vi package.json
~~~

~~~package.json
  "engines": {
    "node": "8.10.0"
  }
~~~

ちなみに node のバージョンを上げないと下記のようなエラーが発生し Build failed になりました

~~~
-----> Installing binaries
       engines.node (package.json):  0.10.x
       engines.npm (package.json):   unspecified (use default)
       
       Resolving node version 0.10.x...
       Downloading and installing node 0.10.48...
       Detected package-lock.json: defaulting npm to version 5.x.x
       Bootstrapping npm 5.x.x (replacing 2.15.1)...
/tmp/build_7437d8fb8310f902d7b8f12dd4a7a3b1/.heroku/node/lib/node_modules/npm/lib/utils/unsupported.js:28
        console.error(`a bug known to break npm. Please update to at least ${r
~~~

ローカルでピンポンできることを確認

~~~
$ bin/hubot
# puppeteer-hubot-mocha-sample> @puppeteer-hubot-mocha-sample ping
# puppeteer-hubot-mocha-sample> PONG
# puppeteer-hubot-mocha-sample> exit
~~~

デプロイ

~~~
$ git add .
$ git commit -m 'Adding hubot'

$ git push heroku master
~~~

続いて HUBOT_SLACK_TOKEN を登録します
slack を開いて上メニューの歯車のアイコンを選択、Add an app

<img width="1207" alt="add_an.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/73acbfba-7dc2-ea0b-61cd-27df48ec8329.jpeg">

hubot と検索

<img width="1438" alt="hubot.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/10ca65ec-fc08-91cb-4fde-775069727aeb.jpeg">

Add Configuration

<img width="1420" alt="add.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/51428661-7304-713f-d80a-c4b6e080803b.jpeg">

Username は puppeteer にしました (slack 上で`@puppeteer`で反応してくれます)

<img width="1027" alt="Screen Shot 2018-03-23 at 7.28.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/106789/d2ace3b5-5663-a2ba-5d04-11a12c8e495a.png">

次の画面の HUBOT_SLACK_TOKEN をメモします

<img width="1021" alt="token.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/277e94b3-a920-1886-4531-8776e7107882.jpeg">

登録

~~~bash 
$ heroku config:add HUBOT_SLACK_TOKEN=xoxb-YOUR_TOKEN
~~~

slack 左メニューの Apps チャンネルで puppeteer を追加します
このチャンネルは自分専用のチャンネルで、発言すると常に`@puppeteer`をつけた状態で発言できます

<img width="211" alt="apps.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/68b0667c-735b-902f-8dd7-b7bc6807bb33.jpeg">

slack 上でもピンポンできることを確認します

<img width="320" alt="pong.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/106789/0ded6d31-198a-9967-2860-becc5b2233ab.jpeg">

ここまででもし問題が起きた場合はログを見ると良いと思います

~~~sh
$ heroku logs
# または
$ heroku logs --tail
~~~

## 3. 設定ファイル作成

conf ディレクトリ以下に設定ファイルを作成してみました

~~~sh
$ mkdir conf
~~~

### slack の設定ファイル

token の取得は [Slack API 推奨Tokenについて](https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21) を参考にさせて頂きました。ありがとうございます！

~~~conf/slack.json
{
  "endpoint": "https://slack.com/api/chat.postMessage",
  "token": "YOUR_APP_TOKEN",
  "userName": "mocha"
}
~~~

### 注文情報の設定ファイル

~~~conf/order.json
{
  "topUrl": "http:///YOUR_EC_CUBE_TOP_PAGE",
  "email": "YOUR_EMAIL",
  "pass": "YOUR_PASSWORD"
}
~~~


## 4. コーディング

さて、コーディングに移ります
私はどうも CoffeeScript に馴染めないので:thinking: node.js で書いてます

### 必要なパッケージをインストール

~~~bash
$ npm install -S child_process mocha chai fs path puppeteer assert request
~~~

### hubot のフロントスクリプト

子プロセスでテストを起動します

~~~scripts/index.js
'use strict';

const ChildProcess = require('child_process');
const request = require('request');
const fs = require('fs');
const path = require('path');

const slackSettingsPath = path.join(__dirname, '../conf/slack.json');
const slackSettings = JSON.parse(fs.readFileSync(slackSettingsPath));

module.exports = (robot) => {
  robot.respond(/test/i, (msg) => {
    // 子プロセスでテストスクリプトを起動
    const result = ChildProcess.execSync('node mocha/index.js').toString();
    // 発言したチャンネルへテスト結果を送信
    request.post(slackSettings.endpoint,
        {
            form: {
                token: slackSettings.token,
                channel: msg.message.room,
                username: slackSettings.userName,
                text: result
            }
        }, (error, response, body) => {
          // 必要に応じてエラー処理等を書く
        }
    );
  });
};
~~~

### テストを実行するためのフロントスクリプト

このまま scripts フォルダ内に書いていくと bot 起動時に実行されてしまうので別途 mocha フォルダを作成、そこに mocha のフロントスクリプトを起きます

~~~mocha/index.js
'use stricts';

const Mocha = require('mocha');
const fs = require('fs');
const path = require('path');

const testDir = path.join(__dirname, '../mocha/test');

const runTest = () => {
  const mocha = new Mocha();

  // テストフォルダの中のテストを自動で追加
  fs.readdirSync(testDir).filter((file) => {
    return file.substr(-3) === '.js';
  }).forEach((file) => {
    mocha.addFile(
      path.join(testDir, file)
    );
  });

  return new Promise((resolve, reject) => {
    mocha.run(failures => {
      resolve(failures);
    });
  });
};

(async () => {
  await runTest();
  process.exit();
})();
~~~

### テストスクリプト

heroku で動かす場合のオプション`'--no-sandbox', '--disable-setuid-sandbox'`がミソかもしれません

~~~mocha/test/order.js
'use stricts';

const fs = require('fs');
const path = require('path');
const puppeteer = require('puppeteer');
const assert = require('chai').assert;

const orderSettingsPath = path.join(__dirname, '../../conf/order.json');
const orderSettings = JSON.parse(fs.readFileSync(orderSettingsPath));

describe('EC-CUBE3 テスト サンプル', function() {
  // mocha タイムアウト
  this.timeout(60000);
  // テストサイトのトップページ
  const topUrl = orderSettings.topUrl;
  // 顧客情報
  const customer = {
    email: orderSettings.email,
    pass: orderSettings.pass

  };
  let browser, page;

  before(async () => {
    // heroku で動かす場合の設定
    const option = process.env.DYNO ? {args: ['--no-sandbox', '--disable-setuid-sandbox']} : {headless: false};

    browser = await puppeteer.launch(option);
    page = await browser.newPage();
  });

  after(async () => {
    await browser.close();
  });

  describe('登録済のアカウントから注文完了できること', async () => {
    // テストデータ
    const dataProvider = {
      '郵便振替': {
        input: {
          payment: '#shopping_payment_1'
        },
        expected: {
          completedUrl: topUrl + '/shopping/complete'
        }
      },
      '現金書留': {
        input: {
          payment: '#shopping_payment_2'
        },
        expected: {
          completedUrl: topUrl + '/shopping/complete'
        }
      },
      '銀行振込': {
        input: {
          payment: '#shopping_payment_3'
        },
        expected: {
          completedUrl: topUrl + '/shopping/complete'
        }
      },
      '代金引換': {
        input: {
          payment: '#shopping_payment_4'
        },
        expected: {
          completedUrl: topUrl + '/shopping/complete'
        }
      }
    };

    // テスト内容
    const exec = async (dataProvider) => {
      // ログアウトから始める
      await page.goto(`${topUrl}/logout`);
      await page.waitFor(1000);

      // 適当な商品をカートに入れる
      await page.goto(`${topUrl}/products/detail/1`);
      await page.select('select[name="classcategory_id1"]', '1');
      await page.select('select[name="classcategory_id2"]', '4');
      await page.click('#add-cart');
      await page.waitFor(1000);

      // レジに進む
      await page.click('a[href*="cart/buystep"]');
      await page.waitFor(1000);

      // ログイン
      await page.type('input[name="login_email"]', customer.email);
      await page.type('input[name="login_pass"]', customer.pass);
      await page.click('form[action*="/login_check"] button[type="submit"]');
      await page.waitFor(1000);

      // 注文する
      await page.click(dataProvider.input.payment);
      await page.waitFor(1500);
      await page.click('#order-button');
      await page.waitFor(1000);

      // 注文完了
      await page.waitFor(1000);
      assert(dataProvider.expected.completedUrl, await page.url());
    };

    // テスト実行
    for (let [describe, testData] of Object.entries(dataProvider)) {
      it(describe, async() => {
        await exec(testData);
      });
    };
  });
});
~~~

## 5. 再度ディプロイ

ディプロイ前に heroku で puppeteer を使うためにビルドパックを追加して完了です
AWS Lambda で puppeteer を使う場合と比べて圧倒的楽ですね:joy:

~~~
$ heroku buildpacks:add https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack
~~~

お疲れ様でした♪

# 今後

- 進行状況を知りたい
- キャンセル機能つけたい
- キャプチャを取ってどこかに保存したい