E2Eテスト基盤開発を担当して

[freee Engineers Advent Calendar 2016](http://qiita.com/advent-calendar/2016/freee)
 12月17日担当の @futoase です。

現在、E2Eテスト基盤構築の担当をしています。[^1]
[Capybara](https://github.com/teamcapybara/capybara)、[SitePrism](https://github.com/natritmeyer/site_prism) および [Selenium](https://github.com/natritmeyer/site_prism) に触れていく中で自分や弊社メンバーから得た知見について記載します。

4点の内容となります。

- Capybara + SitePrism
- Selenium
- E2Eの目的
- E2E基盤構築を担当してみて思ったこと

# Capybara + SitePrism

同僚の @kompiro が [Capybara](https://github.com/teamcapybara/capybara) + [SitePrism](https://github.com/natritmeyer/site_prism) を使うことを提案、フレームワーク化を行ってくれたのでテストケース作成に利用しています。[^2]

[Selenium/Appium Advent Calendar 2016](http://qiita.com/advent-calendar/2016/selenium_and_appium)にてSitePrismを利用したPageObjectsパターンを使ったテスト作成について網羅的に説明した記事を書いています。

[RubyでPageObjectsパターンを実装できる SitePrism のご紹介](http://qiita.com/kompiro/items/7d327dea2fd06342aef0)

## 例

例として[弊社のランディングページ](https://www.freee.co.jp/)に対して
Capybara + SitePrism によるPageObjects, specを書いています。
PageObjectsとテスト(spec)になります。[^3]

### PageObjects

- ログイン画面(secure.freee.co.jp)

```ruby:pages/secure/index.rb
require 'site_prism'

module Secure
  class Login < SitePrism::Page
    set_url 'https://secure.freee.co.jp/users/login/'
  end
end
```

- LP画面(www.freee.co.jp)

```ruby:pages/www/index.rb
require 'site_prism'

module WWW
  # LP
  class Index < SitePrism::Page
    set_url '/'

    element :login_link, 'div.ncms-mod-header1__login a'
  end
end
```

- テスト

```ruby:index_spec.rb
require 'spec_helper'

require 'pages/www/index'
require 'pages/secure/login'

feature 'www.freee.co.jp' do
  context 'click of link for logged in' do

    given(:index_page) { WWW::Index.new }
    subject(:login_page) { Secure::Login.new }

    scenario 'displayed the page of sigin up' do
      index_page.load
      expect(index_page).to be_displayed

      index_page.login_link.click
      expect(login_page).to be_displayed
    end
  end
end
```

## E2Eテストの実行

rspecを実行するとブラウザ(Chrome)が立ち上がり、[www.freee.co.jp](https://www.freee.co.jp/)へアクセスをします。
その後ログインボタンをCapybaraがAPIを経由しclickすることにより[secure.freee.co.jp](https://secure.freee.co.jp/users/login/)に遷移し、ログインページに遷移しているのでテストが正常に終了します。[^4]


```
> bundle ex rspec spec/index_spec.rb
```

![lp.png](https://qiita-image-store.s3.amazonaws.com/0/3985/dc8d8256-949a-df6b-fc2e-0635e616a79d.png)

## 要素の指定

Capybaraではデフォルトでは[css selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)により指定するようになっています。取得したい要素を調べるにはは[chrome devtools](https://developers.google.com/web/tools/chrome-devtools/)を使います。

![anchor.png](https://qiita-image-store.s3.amazonaws.com/0/3985/456a64fd-4147-365b-d838-c478e9def244.png)

![Screen Shot 2016-12-16 at 0.23.24.png](https://qiita-image-store.s3.amazonaws.com/0/3985/257a0110-b6a2-3964-1581-fc78e4c3f198.png)

ログインボタン要素の取得が確認できたら、PageObjectのelementにログインボタンのcss selectorを関連付けを記述します。

```ruby
    element :login_link, 'div.ncms-mod-header1__login a'
```

こうすることで、```index_page.login_link``` という形でログインボタン要素を利用することが可能になります。
```index_page.login_link.click``` にアクセスすれば、マウスクリックイベントが ```index_page.login_link``` に対して発行されます。

### xpath

SitePrismのelementヘルパに :xpath を渡すことで指定することが可能です。

```ruby
    element :login_link, :xpath  "//div[@class='ncms-mod-header1__login']/a"
```

chrome devtoolsでは $x[^5] というヘルパ関数を利用するとxpathを指定し、DOM要素を取得することができます。

![Screen Shot 2016-12-16 at 0.40.58.png](https://qiita-image-store.s3.amazonaws.com/0/3985/23586da0-b353-9009-1144-47944d933cda.png)

### マウスクリック、キーアサイン

Capybaraで、clickや、キーアサインイベントを送る操作などのAPIを用意しています。[^6]
マウスクリックであれば click、エンターキーを押す操作であれば send_keys(:enter) となります。

テストではログインボタンに対しマウスクリックを実行します。

```ruby
      index_page.login_link.click
```

### DOM構築を待つ

非同期処理でfront側がbackend側に通信する仕組みが取り込まれている場合、サイトにアクセスした直後ではAPI問い合わせ中のため、DOMが構築されていない場合があります。その場合、CapybaraはDOM要素を見つけられないため、テストが実行できず、失敗状態となることがあります。[^7]

SitePrismのelement要素で使えるwait_for_ ヘルパを利用すると解決します。

```
      index_page.wait_for_login_link.click
      index_page.login_link.click
```

"wait_for_#{element要素名}" と指定するだけで、elementのDOMが構築されるまで
Capybaraがテスト実行を一時的に停止してくれます。

## デバッグ

### pry

```ruby
bundle ex rspec spec/index_spec.rb 

From: spec/index_spec.rb @ line 14 :

     9:     given(:index_page) { WWW::Index.new }
    10:     subject(:login_page) { Secure::Login.new }
    11:
    12:     scenario 'displayed the page of sigin up' do
    13:       index_page.load
 => 14:       binding.pry
    15:       expect(index_page).to be_displayed
    16:
    17:       index_page.login_link.click
    18:       expect(login_page).to be_displayed
    19:     end
```

指定している要素が見つからないときにテストを一時的にとめるために重宝します。

### current_path

表示しているサイトのパスを確認したいときに利用する。

```ruby
[1] pry > current_path
=> "/
```

PageObjectの[set_url ヘルパ関数](https://github.com/natritmeyer/site_prism/blob/15bbbcff801a8aab0be9de96e3fa99a761c3a802/lib/site_prism/page.rb#L66)にて指定したURLが表示されます。

### URLのパース

AnchorタグからURLの要素を抜き出し、変数に格納したい場合、
[addressable](https://github.com/sporkmonger/addressable)が利用できます。[^8]

```ruby
[1] pry > url = "http://example.com/test#hoge?key=value"
=> "http://example.com/test#hoge?key=value"
[2] pry > parse_url = Addressable::URI.parse(url)
=> #<Addressable::URI:0x3fd6f6dd4c28 URI:http://example.com/test#hoge?key=value>
[3] pry > parse_url.fragment
=> "hoge?key=value"
[4] pry > parse_url.scheme
=> "http"
[5] pry > parse_url.host
=> "example.com"
```

## 成果物

テストフレームワークの成果物はGithubにあげました。
ぜひ参考にしてください。

https://github.com/futoase/capybara-site_prism-sample

# Selenium

E2Eは性格上、テストの実行に時間がかかります。
テストを実行するプロセスを増やすアプローチを取り、並列化することで解消しようと試みています。[^9]
Seleniumを利用してhub, nodeの基盤を構築することでスケールアウトする仕組みを作ることが可能です。

## selenium hub, node

seleniumはhub, nodeモードがあります。[^10]

![selenium.png](https://qiita-image-store.s3.amazonaws.com/0/3985/0a9aca8c-99ea-a47a-5e0c-b954a59645bb.png)

- selenium hub
    - selenium nodeと通信し、node側のブラウザ操作を管理します
    - Capybaraではselenium hubをブラウザとして指定することができます
- selenium node
    - hubとhttpを利用し(API)通信を行い、node上に同居しているブラウザをAPI経由で操作します

## docker-selenium

selenium hub, selenium nodeについて環境を1から構築するのは苦労するので、
[docker-selenium](https://github.com/SeleniumHQ/docker-selenium)という成果物を利用します。

成果物は以下のものがあります。firefox or Chromeがプリインストールされている環境が用意されています。[^11],[^12]

- selenium/node-chrome
- selenium/node-firefox
- selenium/node-chrome-debug
- selenium/node-firefox-debug
- selenium/standalone-chrome
- selenium/standalone-firefox
- selenium/standalone-chrome-debug
- selenium/standalone-firefox-debug

### 試す

試してみるには、dockerの環境が必要です。また、docker-composeを利用します。[^13]

docker-compose.ymlを用意します。[^14]

```yaml:docker-compose.yml
version: "2"
services:
  hub:
    image: selenium/hub:3.0.1-carbon
    ports:
      - "4444:4444"
  chrome:
    image: selenium/node-chrome-debug:3.0.1-carbon
    ports:
      - "15900:5900"
    depends_on:
      - hub
    environment:
      - no_proxy=localhost
      - HUB_PORT_4444_TCP_ADDR=hub
      - HUB_PORT_4444_TCP_PORT=4444
      - HUB_ENV_no_proxy=localhost
```

docker-composeによりdocker containerを立ち上げます。

```
> docker-compose up
```

selenium hub は、 http://localhost:4444/ にアクセスできます。

<img width="689" alt="Screen Shot 2016-12-16 at 20.45.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/3985/c5b2227e-9d87-5ebb-82e3-1c7415ce4e23.png">

selenium nodeへの接続はFinderを立ち上げ、⌘+KでVNCサーバに接続することができます。
(docker-composeの設定で、localhost:15900に対して接続します)

<img width="483" alt="Screen Shot 2016-12-16 at 20.59.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/3985/499725f6-8600-3381-acbf-e7f463b409df.png">

passwordは"secret"と入力し、接続します

<img width="427" alt="Screen Shot 2016-12-16 at 21.05.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/3985/0a136582-7b9d-0d4b-2307-33f64f4dcbd4.png">

<img width="564" alt="Screen Shot 2016-12-16 at 21.15.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/3985/358080d5-dde5-b07a-0bb3-6638e38fbbd4.png">

docker container上で立ち上がっている ubuntu 環境にアクセスできます。

### selenium nodeに対してテストケースを実行するには

selenium hub経由でnodeに対してE2Eテストを実行することができます。
Capybaraの設定でselenium hubを利用するようにすることで可能になります。

```ruby
Capybara.register_driver :selenium_chrome do |app|
  Capybara::Selenium::Driver.new(
    app,
    browser: :remote,
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome,
    url: 'http://localhost:4444/wd/hub'
  )
end

Capybara.current_driver = :selenium_chrome
```

Selenium::WebDriverを利用するように切り替えるだけで、
Capybaraの利用するブラウザがSelenium hubに切り替わります。[^15]

![vnc.png](https://qiita-image-store.s3.amazonaws.com/0/3985/370b928f-cd68-60b4-f561-d8ffe2c4ba30.png)

## nodeを別ホストで運用するには

selenium nodeを別ホストで運用することも可能です。

ただし、docker-seleniumを利用してnodeを立ち上げるには注意点があります。　

node自身がサーブレットにアクセスする必要があるため、5555ポートに対し自分自身(node)を指定する必要があります。そのためのオプション引数を適用するには、[Issueに上がっているパッチの適用が必要(2016/12/17現在)になっています。](https://github.com/SeleniumHQ/docker-selenium/pull/249)

[Selenium HQ wikiにかかれています](http://www.seleniumhq.org/docs/07_selenium_grid.jsp#customizing-the-grid)。

```
Assuming that the node is running on port 5555 all serlvets added to the node are accessible under http://xxx:5555/extra/ path.
```

以下のブログに掲載されていました。

- [参考:各シチュエーションで Selenium Grid を構築するために必要な Node のオプション指定](http://gongo.hatenablog.com/entry/2014/10/29/105755)

SE_OPTSに対し、NICに割り当てられたIPアドレス、ポート番号を渡して立ち上げる形になります。

```yaml:docker-compose.yml
version: "2"
services:
  chrome:
    image: selenium/node-chrome-debug:3.0.1-carbon
    ports:
      - "15900:5900"
    depends_on:
      - hub
    environment:
      - no_proxy=localhost
      - HUB_PORT_4444_TCP_ADDR={selenium hubが動作しているホストのIPアドレス}
      - HUB_PORT_4444_TCP_PORT=4444
      - SE_OPTS=-host {selenium nodeのNICに割り当てられたIPアドレス} -port 5555
```

# E2E構築の目的

E2E基盤はfreeeにおいて以前から存在し、
[QAチームをしばらく1人で切り盛りしていたメンバーが日々のQAテストの中で作り上げていました。](https://keiei.freee.co.jp/2016/11/11/kojinjigyonushi_interview_shain/)
今までのQAチームの流れを組み、人力で解決すべきところは新規機能の箇所に絞ることで効果的なQA体制を築けることを目指しています。
社会に届けているサービスの品質保全をしっかりやるためでもあります。

# E2E基盤構築を担当してみて思ったこと

## 会計ソフトウェアは複雑

会計freee のE2Eテストを書いているのですが、機能の豊富さを毎日実感しています。
テストシナリオに沿ったテストを書くようにしていますが、[^16]

請求書を作成した後取引が作成、その後取引で使われた勘定科目の金額を修正した後に請求書側でどのように見えるのか？

といったテストケースを書いています。これだけでも複雑なテストケース対応が必要です。[^17]

## E2Eテストはもっと泥臭いものだと思っていた

Capybara + SitePrism を使うことで、アプリケーションコードを書いているような感覚でコードのメンテナンス・開発を行えています。
小さなスクリプトを大量に作っていくもの == 事前条件、グローバル変数利用、再利用は難しい ものになるのかと予想していたのですが、予想が覆りました。

テストケースを書いていてかなり楽しいです。

## テストを検証する時間が足りなくなる

1つの操作について、機械的に手早く入力し別の画面に遷移するとAPI通信が間に合わず、ページ遷移後にエラーになることがあります。
そのため適切にsleepや、DOMレンダリング待ちの操作を挟むのですが、その度にテストの速度が遅くなり、テストケースの動作検証に時間がかかるようになります。

localhostでテストケースを動かしてみた後、staging環境でテストケースを動かすと速度差があるためstaging環境だとDOMレンダリングが行われテストが通るケースがありました。

# 総括

- Capybara + SitePrism の組み合わせはかなり良い(2016年12月現在)
- Selenium hub + node の基盤を構築することでテスト実行を並列化し、スケールアウトすることが可能
- エンジニアがこういった基盤構築、テストケース構築にアサインされるのは良いのかなと感じている

# 次のアドベントカレンダー担当は

freee株式会社 フロントエンドエンジニア @tohashi が 2016/12/18(日) の担当です。
[参考プレゼン資料: フロントエンドのモダン化とJavaScriptモジュールの 依存解決](https://speakerdeck.com/tohashi/hurontoendofalsemodanhua-tojavascriptmoziyurufalse-yi-cun-jie-jue)

ご期待ください(｀･ω･´)ゞ

# 注釈

[^1]: 他のプロダクト開発と兼務しています。
[^2]: Capybara は[Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/)などを使ってブラウザを操作するソリューションを提供します。SitePrism はCapybaraに対しPageObjectsパターンを実装できるソリューションを提供します。
[^3]: Gemfileやspec_helperについては省いています。詳しくは成果物をご参考ください。
[^4]:
 テストの実行にはchromedriverが必要です。[homebrew](http://brew.sh/)であればbrew install chromedriver でインストールできます。また、成果物をご利用の場合はbundle installを行ってください。
[^5]: https://developers.google.com/web/tools/chrome-devtools/console/command-line-reference#xpath
[^6]: [Method: Capybara::Node::Element#send_keys](http://www.rubydoc.info/github/jnicklas/capybara/Capybara%2FNode%2FElement%3Asend_keys)
[^7]: jQueryベースのAjax通信を待ちたい場合、Qiita記事 [CapybaraのJSテストがrandom failする](http://qiita.com/saboyutaka/items/cafc2b69ae52f605c8fd)に詳しく記載されています。
[^8]: SitePrismと合わせてインストールされるため、改めてgem installを行う必要はありません。
[^9]: 現在進行系です。Selenium ユーザーの方々に知見を求めにいくかもしれません。
[^10]: 単一マシン上で動作するstandalone モードもあります
[^11]: debugとsuffixが付いているイメージは[VNC環境](https://en.wikipedia.org/wiki/Virtual_Network_Computing)がプリインストールされているものです。
[^12]: ヘッドレスブラウザのphantomjsも[selenium nodeになれます](http://phantomjs.org/api/command-line.html)。docker imageが作成できる[成果物もあります。](https://github.com/futoase/phantomjs2)
[^13]: Ubuntuをご利用されている場合は別途[バイナリをダウンロード](https://docs.docker.com/engine/installation/linux/ubuntulinux/)し、実行できるようにPATHを通す必要があります
[^14]: VNC環境が整ったdocker imageを利用するには理由があり、[IPAフォントがインストールされるようにDockerfileが調整されている](https://github.com/SeleniumHQ/docker-selenium/blob/4dcbab7474402eff96556a82b5fc9c9f8608a49d/NodeChromeDebug/Dockerfile#L37)ため、日本語が文字化けしないためです。
[^15]: desired_capabilities の指定により chrome が動いている node のみテストが実行されるようになります。
[^16]: テストケースは一通りGoogleスプレッドシートに書き、未実装・実装を切り分けて進めています。[PICT](http://www.pairwise.org/)を利用し、テストケースの洗い出し、必要なものの選定をする必要があるのかなと感じています。
[^17]: 対象の償却対象の減価償却率が法令改定前なのか、請求書であれば源泉徴収税は自動計算された状態で表示されているのか、取引先は初回編集時と更新時で変えた結果、pdf出力の内容が変更されているか、など。複雑です。