いもろぐ

思い立ったら書いていくスタイルで

RSpec、Capybaraのお勉強中

このところ、Qiitaの@jnchitoさんの記事を見ながら。RSpec、Capybaraを勉強しています。 何がすごいって、この方の記事がものすごくわかりやすく、いい感じでまとまっていて初学者にとっても優しい感じなのです。

Ruby - 使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita シリーズ

RSpecの入門とその一歩先へ ~RSpec 3バージョン~ - Qiita シリーズ

RSpecもCapybaraも進化していて、何も考えずにGoogle先生に聞くとバージョンの古い記事がHITしてしまって困ったなーと思っていた時に出会ったのでとてもありがたいでございます。

そもそも本職プログラマではない私が無理くり「テストコード書けるようになりたい!」ってやってるので、Rubyで :name の「:」って何よ!?みたいなこととか調べつつやっております。休みの日もあれこれ楽しみで調べては動かしてみてます。とはいえ、早く実戦で使えるようにならないとな。

Ruby on Rails の基礎編終了

年明けから始めていたドットインストールのRails講座、ようやく完了。 初心者のとっかかりとして、このサービスはピカイチだと思う。(他のサービスを使ったことないから比較できないけど)

動画を見ながら、もちろん自分でも書いて動かしてみて、なんだけど一回やっただけだと絶対忘れるし、わかんないところを調べて記録に残しながらやってたので、1レッスン3分なのに、30分くらいかかったりもした、、、まぁ今は時間かかるのはしょうがないと思ってます>< 早く慣れて勘所を掴みたい。(困ったときの調べ方とか)

ドットインストール - 3分動画でマスターする初心者向けプログラミング学習サイト

これでひと通り * SeleniumWebDriver * Rails * RSpec

のホント基本的なところはわかったつもり。 ちょうど社内案件のちょうどいいのがあるので、それをローカル環境(会社のPCね)で動かしてみて、そこにテストコードを書いていく、というのをやる。

っていうところにきて、このプロジェクトはCapybaraでテストが書かれてるみたいなので、ひとまずここにテスト追加できるようになろう。

書籍「実践SeleniumWebDriver」のPageObjectパターンをRSpecでテストコードにしてみた。

このブログは2014/1/1から始めましたが、丸1年経っていったい何ができるようになったんだと。「非開発者がプログラム技術を使ったQAを目指すブログ」って言ってるけどお前ホンキで目指しているのかと。

なんて思いながら新年を迎えてしまいましたが、「Qiitaは「プログラミングに関する知識を記録、共有する最適なサービス」です。」ってことなので、そっちに乗っかってみようかと思いました。あは。

ということで、前回の続きはQiitaで。

書籍「実践SeleniumWebDriver」のPageObjectパターンをRSpecでテストコードにしてみた。 - Qiita

RSpec: beforeで詰まった(itの中から見えるもの見えないもの)

ディレクトリ構成

E2Etest
 └ pages
   └ admin_login_page.rb
 └ spec
   └ test_spec.rb
# admin_login_page.rb
class AdminLoginPage
  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/")
  end
~後略~
# test_spec.rb
require 'selenium-webdriver'
Dir[File.expand_path("../../pages/", __FILE__) << '/*.rb'].each do | file |
  require file
end

describe "ログインして書いて編集して削除するシナリオ" do
  @driver = Selenium::WebDriver.for :firefox

  it "正しいID/PWでログインできること" do
    # ログインページでログインする
    @login_page = AdminLoginPage.new(@driver)

    # loginの戻り値は AllPostsPage
    @all_posts_page = @login_page.login

    # 今回はtitleに「投稿」という文字列が含まれているかで比較
    expect(@driver.title).to include "投稿"
  end
end

を実行すると下記のようなエラーになる

:spec sakaimo$ rspec test_spec.rb 
F

Failures:

  1) ログインして書いて編集して削除するシナリオ 正しいID/PWでログインできること
     Failure/Error: @login_page = AdminLoginPage.new(@driver)
     NoMethodError:
       undefined method `get' for nil:NilClass
     # /Users/sakaimo/mydev/selenium/E2Etest/pages/admin_login_page.rb:5:in `initialize'
     # ./test_spec.rb:13:in `new'
     # ./test_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.00099 seconds (files took 2.96 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./test_spec.rb:10 # ログインして書いて編集して削除するシナリオ 正しいID/PWでログインできること

admin_login_page.rb の initialize に渡される driver が nil みたい。

# test_spec.rb
require 'selenium-webdriver'
Dir[File.expand_path("../../pages/", __FILE__) << '/*.rb'].each do | file |
  require file
end

describe "ログインして書いて編集して削除するシナリオ" do
  before do #←ここ!
    @driver = Selenium::WebDriver.for :firefox
  end

  it "正しいID/PWでログインできること" do
    # ログインページでログインする
    @login_page = AdminLoginPage.new(@driver)

    # loginの戻り値は AllPostsPage
    @all_posts_page = @login_page.login

    # 今回はtitleに「投稿」という文字列が含まれているかで比較
    expect(@driver.title).to include "投稿"
  end
end

で解決しました。

  • 元のコードでは@driverはitの中からは見えない、ってことなのかな。
  • beforeは「exampleの実行前に毎回呼ばれる」からitの中でも見える、ってことなのかな。


ruby: まとめてrequireする

前回↓

書籍「実践SeleniumWebDriver」のPageObjectパターンをRubyでやってみた。 - 非開発者がプログラム技術を使ったQAを目指すブログ

の課題を解決したい

  • ruby 2.0.0p481
  • mac OS10.10.1


E2Etest
  └ pages
    └ add_new_post_page.rb
    └ admin_login_page.rb
    └ all_posts_page.rb
    └ delete_post_page.rb
    └ edit_post_page.rb
  └ test.rb

という構成( 前回と少しディレクトリ構造を変えています)で、test.rbに

#test.rb
require File.expand_path(File.dirname(__FILE__) + '/pages/add_new_post_page')
require File.expand_path(File.dirname(__FILE__) + '/pages/admin_login_page')
require File.expand_path(File.dirname(__FILE__) + '/pages/all_posts_page')
require File.expand_path(File.dirname(__FILE__) + '/pages/edit_post_page')
require File.expand_path(File.dirname(__FILE__) + '/pages/delete_post_page')

って書くのが大変だなという話。


Rubyで指定ディレクトリ以下のファイルを全てrequireする方法 - くろの雑記帳

にまさにドンピシャな内容が書かれていたので参考にさせていただきました。

Dir[File.expand_path("../pages/", __FILE__) << '/*.rb'].each do | file |
  require file
end


以下、自分メモ

File.expand_path('相対Path', __FILE__)
  • "FILE"(現在のファイル。この場合test.rb)を基準として'相対Path'の位置にあるファイルの絶対パスを文字列で返す。
  • ってことは test.rb からみて "../" が "E2Etestディレクトリ" を指すので、結果として
E2Etest/pages/*.rb

のファイルがrequireされる、と。

こちらも参考にさせてもらいました

Rubyを知ろう

RubyのFile.expand_path('相対パス', __FILE__)の意味 - maeharinの日記



書籍「実践SeleniumWebDriver」のPageObjectパターンをRubyでやってみた。

  • 「実践SeleniumWebDriver(初版)」の「9.5 WordPressのエンドトゥエンドのサンプル」がJAVAで書かれてるのでRubyに置き換えようとした。
  • だけど私はJAVARubyもよくわかんないので、会社にあった既存のテストコードを参考にしながら書いてみた。
  • 結果として本に沿ってないし「置き換え」にはなってないっぽい><
  • 正直自信ないので改善点をいただけると嬉しいです。(Rubyの言語仕様も理解不足なので...特にRubyとしての作法とか命名のお約束とか)

前置き

step1:ログインページを操作

  • まずはこんなディレクトリ構成にする。2つのフォルダが同じディレクトリにあるっていうことでスタート。
E2Etest
  └ admin_login_page.rb
  └ test.rb
  • ファイルの中身はこんなん
# admin_logion_page.rb
# ログインページのPageObject
class AdminLoginPage

  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/")
  end

  # ページの機能
  def set_email
    email.clear
    email.send_keys("イーメール")
  end

  def set_pwd
    pwd.clear
    pwd.send_keys("パスワード")
  end

  def click_login_btn
    login_button.click
  end


  # ページの要素
  private

  def email
    return @driver.find_element(:id, "user_login")
  end

  def pwd
    return @driver.find_element(:id, "user_pass")
  end

  def login_button
    return @driver.find_element(:id, "wp-submit")
  end
end
  • 本のP.183によると

    • PageObjectにはユーザーに対するサービスを提供するのであって、ユーザーのアクションを提供するのではない
  • 多分上記のadmin_login_page.rbは「サービス」ではなく「ユーザーのアクション」が書かれてるんだと思う。

  • で、test.rbはこんな感じ。
# test.rb
require 'selenium-webdriver'
require File.expand_path(File.dirname(__FILE__) + '/admin_login_page')

driver = Selenium::WebDriver.for :firefox

# ログインページにログインする
admin_login_page = AdminLoginPage.new(driver)
admin_login_page.set_email
admin_login_page.set_pwd
admin_login_page.click_login_btn
  • コンソールから test.rb を実行してみると動くはず。
$ ruby test.rb

step2:投稿作成ページも作ってみる

  • 投稿の新規作成画面(add_new_post_page.rb)を作成する。
E2Etest
  └ add_new_post_page.rb # ←投稿の新規作成page
  └ admin_login_page.rb
  └ test.rb
# add_new_post_page.rb
# 新規投稿作成画面のPageObject
class AddNewPostPage

  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/post-new.php")
  end

  # ページの機能
  # 本文を入力する
  def set_body(str)
    @driver.switch_to.frame("content_ifr") # WYSIWYGでの入力はframeの切り替えが必要
    body.send_keys(str)
  end

  # タイトルを入力する
  def set_title(str)
    @driver.switch_to.default_content
    title.send_keys(str)
  end

  # 公開ボタンを押す
  def click_publish_btn
    @driver.switch_to.default_content
    publish_btn.click
  end

  # ページの要素
  private
  def body
    return @driver.find_element(:id, "tinymce")
  end

  def title
    return @driver.find_element(:id, "title")
  end

  def publish_btn
    return @driver.find_element(:id, "publish")
  end
end
  • test.rb に記事投稿の処理を追加する
# test.rb
require 'selenium-webdriver'
require File.expand_path(File.dirname(__FILE__) + '/admin_login_page')
require File.expand_path(File.dirname(__FILE__) + '/add_new_post_page')

driver = Selenium::WebDriver.for :firefox

# ログインページにログインする
login_page = AdminLoginPage.new(driver)
login_page.set_email
login_page.set_pwd
login_page.click_login_btn

# 新規投稿
new_post_page = AddNewPostPage.new(driver)
new_post_page.set_title("タイトルです")
new_post_page.set_body("本文です")
new_post_page.click_publish_btn
  • これで新規投稿が可能になった
  • これだと毎回同じタイトル、本文になるので、yyyymmdd_hhmmssとかを追加してユニークな文字列になるようにするtipsはどこかに公開されているはず
  • ちなみに本だとAddNewPostPageの中にaddNewPostというメソッドがあって、PageObjectの中で「記事を書く」というアクションを一つのメソッドでまとめています。

step3:だったら同じようにpageに一連の処理をまとめておけばいいんじゃないか?

# admin_login_page.rb
class AdminLoginPage

  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/")
  end

  # ページの機能
  def login
    email.clear
    email.send_keys("イーメール")
    pwd.clear
    pwd.send_keys("パスワード")
    login_button.click
  end

  # ページの要素
  private

  def email
    return @driver.find_element(:id, "user_login")
  end

  def pwd
    return @driver.find_element(:id, "user_pass")
  end

  def login_button
    return @driver.find_element(:id, "wp-submit")
  end
end
  • 次にadd_new_post_page.rb
# add_new_post_page.rb
class AddNewPostPage

  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/post-new.php")
  end

  # ページの機能
  def add_new_post(titlestr, description)
    @driver.switch_to.frame("content_ifr")
    body.send_keys(description)

    @driver.switch_to.default_content
    title.send_keys(titlestr)

    publish_btn.click
  end


  # ページの要素
  private
  def body
    return @driver.find_element(:id, "tinymce")
  end

  def title
    return @driver.find_element(:id, "title")
  end

  def publish_btn
    return @driver.find_element(:id, "publish")
  end
end
  • 続いてこの2つを呼び出すtest.rbを下記のように変更
# test.rb
require 'selenium-webdriver'
require File.expand_path(File.dirname(__FILE__) + '/admin_login_page')
require File.expand_path(File.dirname(__FILE__) + '/add_new_post_page')

driver = Selenium::WebDriver.for :firefox

# ログインページにログインする
login_page = AdminLoginPage.new(driver)
login_page.login

# 新規投稿
new_post_page = AddNewPostPage.new(driver)
new_post_page.add_new_post("タイトルです", "本文です")
  • ってした後に、test.rbを実行するとさっきと同じように動くはず。
$ ruby test.rb
  • ただこれだと、loginを呼ぶと必ず「正しいID/PWの組み合わせ」でのログイン処理になるので、「間違ったID/PWの組み合わせでログイン出来ないこと」を確認するテストが必要なときは変える必要がありますね。

step4:ページをもっとふやす

  • 残りの下記の画面を追加する

    • 一覧画面(all_post_page.rb)
    • 編集画面(edit_post_page.rb)
    • 削除画面(delete_post_page.rb)
  • 本によると一覧画面( https://xxx.wordpress.com/wp-admin/edit.php )では「以下の6つのサービスを提供します」とのこと

    • 投稿の作成
    • 投稿の編集
    • 投稿の削除
    • カテゴリによる投稿のフィルタリング
    • 投稿内のテキスト検索
    • 投稿数のカウント
  • ってことでこのページは盛りだくさんだけど書いてみる。

# all_posts_page.rb
class AllPostsPage

  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/edit.php")
  end

  # ページの機能
  # 記事の作成
  def create_new_post(title, description)
    # 新規作成画面に遷移
    add_new_post.click

    # 作成画面のPageObjectを作成+投稿
    new_post = AddNewPostPage.new(@driver)
    new_post.add_new_post(title, description)
  end

  # 投稿の編集
  def edit_post(present_title, new_title, description)
    # 指定されたタイトルの詳細画面に行く
    go_to_paticular_post_page(present_title)
    edit_post_page = EditPostPage.new(@driver)
    edit_post_page.editPost(new_title, description)
  end

  # 投稿の削除
  def delete_post(title)
    # 指定されたタイトルの詳細画面に行く
    go_to_paticular_post_page(title)
    delete_post_page = DeletePostPage.new(@driver)
    delete_post_page.delete_post
  end

  # 一覧画面での投稿のフィルタリング
  def filter_posts_by_category(category)
    # 本にも載ってないので割愛。あとで書いてみるかもしれない。
  end

  # 投稿の検索
  def search_in_posts(search_text)
    # 本にも載ってないので割愛。あとで書いてみるかもしれない。
  end

  # 投稿数の取得
  def get_all_posts_count
    return posts_container.find_elements(:tag_name, "tr").size
  end


  # ページの要素
  private

  def add_new_post
    return @driver.find_element(:link_text, "新規追加")
  end

  # 各記事タイトルの要素(という言い方でいいのかな)をすべて取得
  def posts_container
    return @driver.find_element(:id, "the-list")
  end

  # 指定した投稿の編集ページに遷移するメソッド
  def go_to_paticular_post_page(title)
    all_posts = posts_container.find_elements(:class_name, "row-title")
    all_posts.each do |ele|
      if ele.text == title
        ele.click
        break
      end
    end
  end
end
# edit_post_page.rb
class EditPostPage

  def initialize(driver)
    @driver = driver
  end

  def editPost(str_title, str_description)
    @driver.switch_to.frame(content_frame)
    body.clear
    body.send_keys(str_description)

    @driver.switch_to.default_content
    title.clear
    title.send_keys(str_title)

    publish_btn.click
  end


  private
  
  # WYSIWYGのiframe  
  def content_frame
    return @driver.find_element(:id, "content_ifr")
  end

  # 本文入力欄
  def body
    return @driver.find_element(:id, "tinymce")
  end

  # タイトル入力欄
  def title
    return @driver.find_element(:id, "title")
  end

  # 更新ボタン
  def publish_btn
    return @driver.find_element(:id, "publish")
  end
end
# delete_post_page.rb
class DeletePostPage

  def initialize(driver)
    @driver = driver
  end

  def delete_post
    move_to_trush.click
  end

  private

  def move_to_trush
    return @driver.find_element(:link_text, "ゴミ箱へ移動")
  end
end
  • ...と、ここで気づいたこと
    • 本をよく見たら admin_login_pageにあるloginメソッドって戻り値としてAllPostsPageクラスを返してるのね。。。見落としてたわ_| ̄|○
    • ってことで下記のように修正
# admin_login.rb
class AdminLoginPage

  def initialize(driver)
    @driver = driver
    @driver.get("https://xxx.wordpress.com/wp-admin/")
  end

  # ページの機能
  def login
    email.clear
    email.send_keys("イーメール")
    pwd.clear
    pwd.send_keys("パスワード")
    login_button.click

    # ログイン後にはAllPostPageを返す
    return AllPostsPage.new(@driver)
  end


  # ページの要素
  private

  def email
    return @driver.find_element(:id, "user_login")
  end

  def pwd
    return @driver.find_element(:id, "user_pass")
  end

  def login_button
    return @driver.find_element(:id, "wp-submit")
  end
end
  • そしてtest.rbをこうしてみる
# test.rb
require 'selenium-webdriver'
require File.expand_path(File.dirname(__FILE__) + '/admin_login_page')
require File.expand_path(File.dirname(__FILE__) + '/add_new_post_page')
require File.expand_path(File.dirname(__FILE__) + '/all_posts_page')
require File.expand_path(File.dirname(__FILE__) + '/edit_post_page')
require File.expand_path(File.dirname(__FILE__) + '/delete_post_page')


driver = Selenium::WebDriver.for :firefox

# ログインページにログインする
login_page = AdminLoginPage.new(driver)

# loginの戻り値は AllPostsPage
all_posts_page = login_page.login


# 記事の投稿
all_posts_page.create_new_post("タイトル1", "本文1")

# 記事の編集
driver.get("https://xxx.wordpress.com/wp-admin/edit.php")
all_posts_page.edit_post("タイトル1", "タイトル2", "本文2")

# 記事数の取得
driver.get("https://xxx.wordpress.com/wp-admin/edit.php")
puts "記事数 = #{all_posts_page.get_all_posts_count}"

# 記事の削除
driver.get("https://xxx.wordpress.com/wp-admin/edit.php")
all_posts_page.delete_post("タイトル2")
  • ここまでのディレクトリの中
E2Etest
  └ add_new_post_page.rb
  └ admin_login_page.rb
  └ all_posts_page.rb
  └ delete_post_page.rb
  └ edit_post_page.rb
  └ test.rb

課題

  • 課題1:pageの数だけrequieが増えるのか!? → 調べればすぐできそう
  • 課題2:PageObjectすべてにURLをフルパスで書くと変更したとき大変 → 調べればすぐできそう
  • 課題3:画面遷移をするときに、遷移先のpageオブジェクトをnewするとき、しないときってどう使い分けるのかわかってない → これは答えがよくわかんない

  • これらは「操作の自動化」であって「テストの自動化」ではないので、今後はRSpecをつかってテストの自動化をしてみたいと思います。

  • 冒頭にも書きましたが、いい書き方なのか不安なのでアドバイスお願いします!

コードがあたり前の世界

世界を変えよう。アップルによるキッズ向けコーディングワークショップ : ギズモード・ジャパン

の記事を読んで思ったこと。

世界を変える手段はコードだけじゃないだろうけど、もはやプログラミングスキルは義務教育レベルに当たり前スキルになってくるとしたときに、自分の子供に教えてあげられる(可能であればレベルの高いものを優しく)ようになってたいし、一緒にプログラミングを楽しめるようになりたい。

例えば今、年齢高い人でPC使えない人って、どこかであきらめたんだと思います。私には無理だから。あるいは(それまでの)仕事をやる上で必要ないから。でも現実的にそゆ人は組織の生産性を下げてると思います。

これは”使う”話ですが、今後は”作る”のが当たり前な世界になるんじゃなかろうか。例えば自分に期待されるアウトプットがメールやエクセルじゃなくてコードになる。営業も経理もデザインすらコードになる、みたいな世界。