14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

前提

事前準備

環境設定

bundle install

C:\E2E_tw>bundle init
Writing new Gemfile to C:/E2E_tw/Gemfile
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の中をみるとこうなっています。

.rspec
--color
--require spec_helper

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

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

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

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

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

何をテストするのか

Twitterで

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

を確認してみます。

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

Selenium2でつくるテストケースの構成についてより
PageObjectDesignPattern.png

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

  • 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

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

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 にあります。通常なら要素の特定の時に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) をしてなくて動かない時間が数時間ありました。pageobjecinitialize の処理は page-object/lib/page-object.rb にあります。(今の私のスキルでは何やってるかよくわかんないけど_| ̄|○)

timeline_page

timeline.png

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

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

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

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に対して操作をするクラスです。

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_successlogin_errorがあるの?という話ですが、PageObject
デザインパターンでは画面遷移を表現するときに、遷移先のpageを返すってのがセオリーだそうです。ですのでログイン成功後に遷移するpage、あるいはログイン失敗後に遷移するpage、を返す必要があります。

login_data

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

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

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

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あったらまた投稿しますー。疑問、質問、アドバイスなどありましたら教えて下さい!

14
16
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?