SeleniumWebDriver+RSpecでのE2Eテスト自動化をPageObjectデザインパターンで作る

過去に [書籍「実践SeleniumWebDriver」のPageObjectパターンをRSpecでテストコードにしてみた。](http://qiita.com/sakaimo/items/5af928aaae5a513c6b08) で「写経」はしてみたのですが、いよいよ実戦で使う時が来たのでこれを思い出す意味で書いていこうと思います。(前任者が残してくれたコード、資料があったのでここまでできるようになりました)

# 前提

- SeleniumWebDriverとRubyとRSpecはそこそこわかる
- PageObjectデザインパターンもそこそこわかる
    - これについては前任者が書いてくれた「[Selenium2でつくるテストケースの構成について](http://www.slideshare.net/ssusere4f193/selenium2-36100497)」 がものすごくわかりやすいです。
- 環境は諸事情によりWindows8.1 (多分Linuxでも同じことができるハズ)ブラウザが立ち上がるのでGUIがあったほうがいいです。
- headlessでもできるみたいです
    - 「[Seleniumをブラウザなしで起動するための方法を調べた](http://futurismo.biz/archives/2474)」


# 事前準備

## 環境設定

- [コマンドプロンプト起動時、自動的に文字コードをUTF-8にして日本語もちゃんと表示できるようにする方法](http://qiita.com/yui/items/86a455f948a3e1c0ef97) を参考に、コマンドプロンプトの文字コードをUTF-8に設定しておく。私は Step:2-B の方法を取りました。

- [Windows で Ruby(Railsじゃない環境)でTDDで開発をするための環境構築 2012/8月版](http://qiita.com/yujiorama_at_github/items/49c8219c6062aed096fe) を参考に下記を準備しました
    - Ruby 2.2.2　を入れる
    - DevKitも入れる
    - Bundler 1.10.3を入れる
- E2E_twディレクトリ作って、その中で作業

## bundle install

```
C:\E2E_tw>bundle init
Writing new Gemfile to C:/E2E_tw/Gemfile
```

```rb:Gemfile
source "https://rubygems.org"

gem "selenium-webdriver"
gem "rspec"
gem "rspec_junit_formatter"
gem "page-object"
```

page-objectっていうモジュールが優れものっぽいです。使ってみます。
https://github.com/cheezy/page-object/wiki/page-object

bundle install します。

```
C:\E2E_tw>bundle install --path vendor/bundle
```

## rspec

rspecもinitしておきます。

```
C:\E2E_tw>rspec --init
  create   .rspec
  create   spec/spec_helper.rb
```

`.rspec`の中をみるとこうなっています。

```rb:.rspec
--color
--require spec_helper
```

`--color` 出力に色を付けてくれます
`--require spec_helper` すべての\_spec.rb ファイルに require spec_helper してくれます

requireされる`spec_helper`に下記のように書いておきます。

```rb:spec/spec_helper.rb
require "rubygems"
require "rspec"
require "page-object"
require "selenium-webdriver"

Dir[File.join(File.dirname(__FILE__), "{pages,operators,support,fixtures}/*.rb")].each{ |f| require f  }

RSpec.configure do |config|
# ～デフォルトのまま～
end

```

```rb
Dir[File.join(File.dirname(__FILE__), "{pages,operators,support,fixtures}/*.rb")].each{ |f| require f  }
```

って書くと、一気にrequireできちゃうんですねスゴイ。

# 何をテストするのか

Twitterで

- 有効なアカウントでログインできること
- 無効なアカウントでログインできないこと

を確認してみます。


# コードの設計(構成？)としてはこう

[Selenium2でつくるテストケースの構成について](http://www.slideshare.net/ssusere4f193/selenium2-36100497)より
![PageObjectDesignPattern.png](https://qiita-image-store.s3.amazonaws.com/0/8612/a683e85c-f673-104e-4262-51b8755af685.png)


今回はやることが少ないので`operators`と`fixtures`に分ける必要は無いのですが、あえてこの設計で書いてみようと思います。規模が大きくなるとこっちのほうがいいのかなと思いまして。

- features
    - テストシナリオ(テストケース？)
    - どの画面で、コレして、ソレして、アレしたら、こうなるよね、を書く
- pages
    - テスト対象の画面を表現するクラス
    - その画面に対して操作できることを書く
- operators
    - ページに対する処理をここに書く
    - 書き方いろいろあると思うのですが、今回は「pageに対する操作はすべてoperatorを通して行う」で書いてみました
- fixtures
    - テストで使うデータセットを書く



```
C:\E2E_tw
│   .rspec
│
└───spec
    │   spec_helper.rb
    │
    ├───features
    │       login_spec.rb
    │
    ├───fixtures
    │       login_data.rb
    │
    ├───operators
    │       login_operator.rb
    │
    └───pages
            login_error_page.rb
            login_page.rb
            timeline_page.rb
```

本当はテスト(\_spec.rb)から書き始めるのがテストドリブンぽくていいのかもしれませんが、説明上、pageにこんなものがあって、それをspecでこう使ってるよ、っていうほうがわかりやすいと思いまして、pageから説明します。

## login_page

![login.png](https://qiita-image-store.s3.amazonaws.com/0/8612/11280b5d-538b-0d0f-7f01-d529bbaf7a57.png)

↑ログインページを表現するクラスです。

```rb:spec/pages/login_page.rb
require 'page-object'

class LoginPage

  include PageObject

  # テストで使う要素を特定
  text_field(:id, :id => 'signin-email')
  text_field(:pw, :id => 'signin-password')

  #button(:submit, :value => 'ログイン') # help me !
  button(:submit, :xpath => '//*[@id="front-container"]/div[2]/div[2]/form/table/tbody/tr/td[2]/button')

  def initialize(driver)
    super(driver)
    @driver = driver
    @url = "https://twitter.com/"
  end

  def open
    @driver.get(@url)
  end

  def login_success(id, pw)
    self.id = id
    self.pw = pw
    self.submit

    result_page = TimelinePage.new(@driver)
    return result_page
  end

  def login_error(id, pw)
    self.id = id
    self.pw = pw
    self.submit

    result_page = LoginErrorPage.new(@driver)
    return result_page
  end
end
```

PageObjectの使い方は [cheezy/page-object](https://github.com/cheezy/page-object/wiki/Get-me-started-right-now!) にあります。通常なら要素の特定の時に`find_element`しますが、`text_field(:id, :id => 'signin-email')` ように簡単に書けるのが魅力的。

その使い方によれば、上記のログインボタンの要素特定には

```
  #button(:submit, :value => 'ログイン') # help me !
```

と書けるはずなのですが、動かないので仕方なくxpathで指定しています。(誰か教えて！)


`@url`(自分のページを表すurl)をpageに持たせたかったので `initialize` メソッドを作ったのですが、PageObjectの使い方のサイトに

> Do not create your own initialize method as one already exists and should not be overwritten.

って書いてあったのに `super(driver)` をしてなくて動かない時間が数時間ありました。`pageobjec` の `initialize` の処理は [page-object/lib/page-object.rb](https://github.com/cheezy/page-object/blob/master/lib/page-object.rb#L66-L71) にあります。(今の私のスキルでは何やってるかよくわかんないけど＿|￣|○)

## timeline_page

![timeline.png](https://qiita-image-store.s3.amazonaws.com/0/8612/979843fe-4e40-7ce0-30ee-c7c927d03a3c.png)


ログイン成功後の画面を表現するクラスです。

```rb:spec/pages/timeline_page.rb
require 'page-object'

class TimelinePage

  include PageObject

  # 自分のidを表示する要素
  span(:my_id, :class => 'u-linkComplex-target')

  def initialize(driver)
    super(driver)
    @driver = driver
  end

  # id名を取得する
  def get_id
    return self.my_id
  end
end
```

テストで使う要素の特定と、テストで必要な操作( `get_id` )を書きました。



## login_error_page

![login_error.png](https://qiita-image-store.s3.amazonaws.com/0/8612/c0b185d4-fd8e-0ea2-6277-15539510d8a8.png)

↑ログインに失敗したときの画面を表現するクラスです。

```rb:spec/pages/login_error_page.rb
require 'page-object'

class LoginErrorPage

  include PageObject

  # エラーメッセージが表示される要素
  span(:error_msg, :class => 'message-text')

  def initialize(driver)
    super(driver)
    @driver = driver
  end

  def get_error_msg
    return self.error_msg
  end
end

```

テストで使う要素の特定と、テストで必要な操作( `get_error_msg` )を書きました。

## login_operator

LoginPageに対して操作をするクラスです。

```rb:spec/operators/login_operator.rb
class LoginOperator

  def initialize(driver)
    @driver = driver
  end

  def login_success(page, id, pw)
    return page.login_success(id,pw)
  end

  def login_error(page, id, pw)
    return page.login_error(id,pw)
  end
end
```

何故に`login_success`と`login_error`があるの？という話ですが、PageObject
デザインパターンでは画面遷移を表現するときに、遷移先のpageを返すってのがセオリーだそうです。ですのでログイン成功後に遷移するpage、あるいはログイン失敗後に遷移するpage、を返す必要があります。



## login_data

テストで使うデータたちを書いておきます。今回はログインするためのID/PWのセットになります。

```rb:spec/fixtures/login_data.rb
module LoginData
  module_function

  # 登録済ユーザー
  def registered_user
    data = {
        id: "twitter_id",
        pw: "twitter_pw",
    }
    return data
  end

  # 未登録ユーザー
  def unknown_user
    data = {
        id: "mitourokuyu-za-",
        pw: "mitouroku-password",
    }
    return data
  end
end
```

## login_spec

いよいよここまでの材料(?)を使ってテストを書くところです。


```rb:spec/features/login_spec.rb
describe "LoginPage" do

  before do
    # ドライバ作って
    @driver = Selenium::WebDriver.for :firefox

    # ページ作って
    @login_page = LoginPage.new(@driver)

    # オペレータ作って
    @login_op = LoginOperator.new(@driver)

    # ページ開く
    @login_page.open
  end

  after do
    @driver.quit
  end

  context '有効アカウント' do
    it 'ログインできること' do

      # 入力するデータを準備
      input_data = LoginData.registered_user

      # ログインする(ログイン後のpage=タイムライン画面 が返ってくる)
      timeline_page = @login_op.login_success(@login_page, input_data[:id], input_data[:pw])

      # タイムライン画面の自分のidが表示されているところにある文字列を取得
      my_id = timeline_page.get_id

      # 期待値と比較する
      expect(my_id).to eq 'twitter_id'
    end
  end

  context '無効アカウント' do
    it 'ログインできないこと' do
      input_data = LoginData.unknown_user

      # ログインしようとする
      login_error_page = @login_op.login_error(@login_page, input_data[:id], input_data[:pw])
      error_msg = login_error_page.get_error_msg
      expect(error_msg).to eq '入力されたユーザー名またはパスワードに誤りがあります。ダブルクリックして再度お試しください。'
    end
  end
end
```

(RSpecの書き方とか、もっといいのがあったら教えてほしいです...)

## テストを実行する

`-fd`オプションを付けて実行してみました。

```
C:\E2E_tw>rspec -fd

LoginPage
  有効アカウント
    ログインできること
  無効アカウント
    ログインできないこと

Finished in 24.52 seconds (files took 0.81775 seconds to load)
2 examples, 0 failures
```

ブラウザが立ち上がって、twitterにログインできるはずです。

# まとめ

というように書いてみたのですがいかがでしょうか。実戦で使ってみて、何かtipsあったらまた投稿しますー。疑問、質問、アドバイスなどありましたら教えて下さい！