Let's Enjoy PhantomJS

この記事は [Retty Advent Calendar](http://qiita.com/advent-calendar/2016/retty) 1６日目です。
昨日は[Eisuke](http://qiita.com/eisuke_watanabe)さんの[デザイナー向け - コンポーネント化について考える](http://qiita.com/eisuke_watanabe/items/0a13544cc0f906557b84)でした。

スクレイピング大好きおじさんです。おはようございます。

Rettyではいろいろな技術が使われています。実際にフロントエンドでサービスを動かしているPHP/Zend Frameworkを始めとしてみんな大好き機械学習までさまざまなものを利用しています。

私はバックエンドを主に担当しています。利用されているユーザーの方々が見ていない裏側のプロセスを色々ゴニョゴニョしています。

尖った技術に関しては他の方々にお任せして、今回は私が普段使っている **[PhantomJS](http://phantomjs.org/)** を軽くご紹介したいと思います。
Docker初心者・PhantomJS初心者向けの記事です。


兎角テストとはめんどくさいものです。
私はコードを書くのは好きなのですが、テストをするのはあまり好きではありません。

テストにはご存知のようにいくつかのフェーズがあり、単体テスト・結合テスト・システムテストなどがあります。

今から10年位前のSIer時代の話です。
テスト項目を作成し、テスト項目を上長にレビューしてもらい、テスト項目を元に画面の操作を行い、1ステップごとにキャプチャソフトで1枚ずつスクリーンショットを撮り、プリントアウトして上長に確認してもらっていた…という忌まわしい記憶が蘇ります…。

今はそんなアコギな作業をしなくてもテストを行うSolutionが！　Test Frameworkがあるのです！　なんと良い時代になったのでしょうか！ Viva Innovation !!

導入は既に他社の方が公開されているものを参考にしました。
（主な参考元： [Java+PhantomJSでサイトレスポンスを計測する : アジャイル株式会社](https://www.agilegroup.co.jp/technote/java-phantomjs.html]) ）

私が実行しているのはオンプレマシンに必要な環境を用意して、都度必要なタイミングで処理をまとめたスクリプトを実行しています。
上述の参考元でもJavaを使っていますが、私のPhantomJSもJavaから呼び出しています。
元々Rubyで書いてあったコードがあったのですが、チェックする対象のページが膨大で直列で実行すると1日近くかかってしまっていたということもあり、マルチスレッドで実行可能かつ自分の書きやすい言語と言うことでJavaを選びました。

そういった経緯があり、Java, PhantomJSが最低限必要なものとして上がって来ました。

実行環境はUbuntu 14.04.5なので基本 `apt-get` コマンドで殆ど必要なソフトウェアはインストールすることが出来ました。

実際に私がインストール作業を行ったもの、行っていないものとあるので、Dockerで同じ環境を作ってみよう！と言うことで進めていきたいと思います！

# PhantomJSの実行環境を作ろう！

## Docker を手に入れよう！

[公式サイト](https://docs.docker.com/)からダウンロードしてください。
英語サイトですが、別に難しいことが書いてあるわけではないのでサクッと必要なページからダウンロードしましょう！
Macの場合は **Topページ** → 左のメニューの **Docker for Mac** → **Getting Started** 

インストールして、次はubuntuのDockerイメージを手に入れましょう。

ただ、Java8のコードはubuntuの標準パッケージとして用意されていないようで、インストールがとても煩雑で骨が折れます。
ここは横着をしてJava8がインストール済みのDokcerイメージを利用しようと思います。

```bash
$ docker pull openjdk:8
8: Pulling from library/openjdk
75a822cd7888: Pull complete
57de64c72267: Pull complete
4306be1e8943: Pull complete
1e6944bfb718: Pull complete
3521f2f45ed2: Pull complete
9c2f0d9b5f90: Pull complete
8cedcf6d2527: Pull complete
31f99da7583d: Pull complete
Digest: sha256:6bfae6cf8902a28d3f0209f5cdbb269032762a94835d331811df33498970aa7e
Status: Downloaded newer image for openjdk:8
```

## Dockerの立ち上げ
Docker pullしてダウンロードしたDockerイメージを確認します。

```bash
$ docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
openjdk                           8                   a001fc27db5a        30 hours ago        643 MB
ubuntu                            latest              4ca3a192ff2a        2 weeks ago         128 MB
harisekhon/centos-java            latest              d2f082b5ad2d        3 months ago        397 MB
centos                            latest              970633036444        4 months ago        197 MB
tracker/cron-resource             latest              8210be87b158        5 months ago        20.8 MB
redis                             latest              4465e4bcad80        6 months ago        186 MB
hello-world                       latest              693bce725149        6 months ago        967 B
nginx                             latest              0d409d33b27e        6 months ago        183 MB
behance/docker-gocron-logrotate   latest              3b514e40da91        18 months ago       223 MB
dockerbase/cron                   latest              df6071e77600        2 years ago         312 MB
```

では早速起動

```bash
$ docker run -it a001fc27db5a /bin/bash


$ java -version
openjdk version "1.8.0_111"
OpenJDK Runtime Environment (build 1.8.0_111-8u111-b14-2~bpo8+1-b14)
OpenJDK 64-Bit Server VM (build 25.111-b14, mixed mode)

```

ついでにOSの確認

```bash
$ cat /etc/issue
Debian GNU/Linux 8 \n \l
```

## Mavenのインストール

私はJavaの[Maven](https://maven.apache.org/)というフレームワーク上でPhantomJSを実行しています。

> [Apache Maven - Wikipedia](https://ja.wikipedia.org/wiki/Apache_Maven)から引用
>
> Apache Maven（アパッチ メイヴン／メイヴェン）は、Java用プロジェクト管理ツールである。Apache Antに代わるものとして作られた。Apacheライセンスにて配布されているオープンソースソフトウェアである。

Mavenについては主題と離れてしまうので、この記事では割愛します。
apt-getコマンドからも取得できるようなのですが、パッケージの依存関係でインストールが中断されることが多いので
動作実績があるファイルをダウンロード・展開して利用します。

```bash
$ wget http://ftp.riken.jp/net/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz
$ tar xzvf apache-maven-3.3.9-bin.tar.gz
```

## PhantomJSのインストール
Maven同様、こちらも動作実績のあるファイルをダウンロード・展開して利用します。

```bash
$ wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
$ bzip2 -dc phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xvf -
```

# 実行してみよう！

Mavenで実行するためのライブラリの設定ファイル（pom.xml）を用意します。

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>PhantomJSSampleOnMaven</groupId>
    <artifactId>PhantomJS</artifactId>
    <version>1.0</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.github.detro.ghostdriver</groupId>
            <artifactId>phantomjsdriver</artifactId>
            <version>1.1.0</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>net.arnx</groupId>
            <artifactId>jsonic</artifactId>
            <version>1.3.10</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <configuration>
                    <mainClass>Main</mainClass>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>Main</mainClass>
                            <addClasspath>true</addClasspath>
                            <addExtensions>false</addExtensions>
                            <classpathPrefix>lib/</classpathPrefix>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy</id>
                        <phase>install</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.directory}/lib
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
```

そして実際にPhantomJSをJavaから呼び出すためのクラスです。[前述の参考元のソース](https://www.agilegroup.co.jp/technote/java-phantomjs.html)を元に若干いじったものです。
こちらは[Yahoo! Japan](http://www.yahoo.co.jp)のページ内に含まれる特定のDivタグを確認するサンプルになります。

```java:Main.java
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriverService;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.util.Optional;

public class Main {

  private static final String PHANTOMJS_PATH = System.getenv("PHANTOM_JS_PATH");
  private static final String[] PHANTOM_ARGS =
      new String[]{
          "--webdriver-loglevel=NONE",
          "--load-images=no"
      };
  private static final int TIMEOUT_SECONDS = 30;

  public static void main(String[] args) {
    Main testEngine = new Main();
    testEngine.check("http://www.yahoo.co.jp");
  }

  private PhantomJSDriver initDriver() {
    // set Capabilities
    DesiredCapabilities capabilities = DesiredCapabilities.phantomjs();
    capabilities.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS, PHANTOM_ARGS);
    capabilities.setCapability("phantomjs.page.settings.resourceTimeout", TIMEOUT_SECONDS);
    capabilities.setJavascriptEnabled(true);

    System.setProperty("phantomjs.binary.path", PHANTOMJS_PATH);
    PhantomJSDriver driver = new PhantomJSDriver(capabilities);
    return driver;
  }


  public void check(String url) {

    long processStartTime = System.currentTimeMillis();
    PhantomJSDriver driver = null;

    try {
      driver = this.initDriver();
      driver.get(url); // access to specified URL

      waitForLoad(driver);

      // Get values of Navigation Timing
      long startTime = (Long) driver.executeScript("return window.performance.timing.navigationStart");
      long loadEndTime = (Long) driver.executeScript("return window.performance.timing.loadEventEnd");
      long responseEndTime = (Long) driver.executeScript("return window.performance.timing.responseEnd");

      Optional<WebElement> tag = this.findTag(driver);
      if (tag.isPresent()) {
        System.out.println("Find !!");
      } else {
        System.out.println("Could not find ...");
      }
      System.out.format("Response Time : %d\n", responseEndTime - startTime);
      System.out.format("PageLoad Time : %d\n", loadEndTime - startTime);
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (driver != null) {
        driver.quit();
      }
    }

    if (driver != null) {
      System.out.println("Destroy phantomJsDriver.");
      driver = null;
    }
  }

  private void waitForLoad(PhantomJSDriver driver) throws Exception {
    ExpectedCondition<Boolean> pageLoadCondition = new
        ExpectedCondition<Boolean>() {
          public Boolean apply(WebDriver driver) {
            return ((JavascriptExecutor) driver).executeScript("return document.readyState").equals("complete");
          }
        };
    try {
      WebDriverWait wait = new WebDriverWait(driver, TIMEOUT_SECONDS);
      wait.until(pageLoadCondition);
    } catch(TimeoutException e) {
      throw e;
    } catch (Exception e) {
      e.printStackTrace();
      throw e;
    }
  }

  private Optional<WebElement> findTag(PhantomJSDriver driver) {
    if (driver == null) {
      System.out.println("Driver is not set...");
      return Optional.empty();
    }
    try {
      WebElement element = driver.findElement(By.id("masthead"));
      return Optional.ofNullable(element);
    } catch (Exception e) {
      return Optional.empty();
    }
  }
}
```

あとは実行するためのシェルを用意。
今回はLibディレクトリにPhantomJSとMavenのディレクトリを移動せず、解凍した場所から移動していないので環境変数にセットする必要があります。
それから `mvn package` 、 `mvn exec:java -e` で実行することが出来ます。

```bash:run.sh
#!/bin/bash

export PHANTOM_JS_PATH=/phantomjs-2.1.1-linux-x86_64/bin/phantomjs
# maven
export M3_HOME=/apache-maven-3.3.9
export M3=$M3_HOME/bin
export PATH=$M3:$PATH

mvn package
mvn exec:java -e
```

## 実行結果
```
$ ./run.sh

[INFO] ------------------------------------------------------------------------
[INFO] Building PhantomJS 1.0
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.5.0:java (default-cli) @ PhantomJS ---
Dec 15, 2016 9:16:11 AM org.openqa.selenium.phantomjs.PhantomJSDriverService <init>
INFO: executable: /phantomjs-2.1.1-linux-x86_64/bin/phantomjs
Dec 15, 2016 9:16:11 AM org.openqa.selenium.phantomjs.PhantomJSDriverService <init>
INFO: port: 1511
Dec 15, 2016 9:16:11 AM org.openqa.selenium.phantomjs.PhantomJSDriverService <init>
INFO: arguments: [--webdriver-loglevel=NONE, --load-images=no, --webdriver=1511, --webdriver-logfile=/PhantomJSSample-master/phantomjsdriver.log]
Dec 15, 2016 9:16:11 AM org.openqa.selenium.phantomjs.PhantomJSDriverService <init>
INFO: environment: {}
Find !!
Response Time : 654
PageLoad Time : 2101
Destroy phantomJsDriver.
```

このサンプルでは特定のDIVタグがあるかどうかを確認するだけですが、画面のキャプチャの取得も行うことも出来る上、プログラムでページごとに異なるチェックを行うなど色々広げていくことが出来ます。

私はRettyで提供しているページに特定のDivブロックが出ているか、間違ったページにお店の情報が出ていないかのチェックとして利用しています。

今回はJavaでの実行環境の作成から実施までを紹介しましたが、もちろん個人で使用しているMacにインストールして実行することも可能です。

さようなら人力テスト！　こんにちはPhantomJS！

これであなたのテスト工数も激減☆(ゝω･)v