CLOVER🍀

That was when it all began.

Groovy(Geb)とScala(ScalaTest)を使ってSeleniumで遊ぶ

最近、GebというGroovyでSeleniumを簡単に使えるものが流行っているらしいですね。名前もよく見るので、ちょっと試してみることにしました。

あと、どうせならということでScalaでもSeleniumを簡単に使うというアプローチがあればということで調べてみたら、ScalaTestが挙がったのでこちらも合わせて。

このブログでSeleniumといえば、以前にSeleniumそのもので遊んでみました的なエントリを書いたことがあったようです。

Selenium WebDriverで遊ぶ
http://d.hatena.ne.jp/Kazuhira/20130103/1357213044

今回は、こちらをGroovy(Geb)とScala(ScalaTest)で置き換えてみましょう。

別に本題と関係ないんですけど今回Seleniumを使うにあたり、Firefox 36とselenium-firefox-driver 2.44.0以下の組み合わせだとSeleniumがクラッシュしてハマりました。

Issue 8399: Firefox 36 breaks WebDriver 2.44.0
https://code.google.com/p/selenium/issues/detail?id=8399

仕方ないのでしばらく放置かなぁと思ったら、翌日に2.45.0がリリースされていて、めでたく続行可能になりました。なにか、引きが強いようです…。

では、続けます。

やることは、2つのシナリオです。ブラウザは、Firefoxとします。

  • Googleにアクセスしてタイトル確認後、「Cheese!」と入力してサブミット。遷移後のタイトルを確認
  • 上記に加えて、画面表示時点のスクリーンショットを撮る

Geb

Groovyで、Seleniumを簡単に使えるようにするライブラリらしく、直接Seleniumを使うよりもコードが簡単になったり、jQueryライクなセレクタが使えたりといろいろ便利なようです。

Geb
http://www.gebish.org/

発音は、"Jeb”だそうな。

Gebを使う時には、Browserというクラスを使うようなのですが、Browser#driveにClosureを渡す書き方と、普通にBrowserのインスタンスに対してメソッド呼び出ししていく書き方があるようです。今回は、これを交互に書いてみました。

まずは最初のシナリオ。こちらは、Browser#driveにClosureを渡すスタイルで書いています。

@Grab('org.gebish:geb-core:0.10.0')
@Grab('org.seleniumhq.selenium:selenium-firefox-driver:2.45.0')
@Grab('org.seleniumhq.selenium:selenium-support:2.45.0')
import geb.Browser
import org.openqa.selenium.Keys
import org.openqa.selenium.support.ui.ExpectedCondition
import org.openqa.selenium.support.ui.WebDriverWait

Browser.drive {
  go 'https://www.google.co.jp/'

  assert title == 'Google'

  $('input[name="q"]').with {
    value('Cheese!')
    it << Keys.chord(Keys.ENTER)
    // firstElement().submit()  // SeleniumのWebElementを使う場合はこちら
  }

  def pred = { d ->
    d.title.toLowerCase().startsWith("cheese!") } as ExpectedCondition
  (new WebDriverWait(driver, 10)).until(pred)

  assert title == 'Cheese! - Google 検索'
}.quit()

依存関係の解決には、Grapeを使っています。

最初、Javaのコードをまともに移植しようとして、submitが見つからなくて考え込んだりしていました。ドキュメントの渡り歩き方にもまだ慣れがないですからねぇ…。

続いて、スクリーンショットを撮る方。こちらは、Browserのインスタンスに対して、メソッド呼び出しする方向で。

@Grab('org.gebish:geb-core:0.10.0')
@Grab('org.seleniumhq.selenium:selenium-firefox-driver:2.45.0')
@Grab('org.seleniumhq.selenium:selenium-support:2.45.0')
import geb.Browser
import org.openqa.selenium.Keys
import org.openqa.selenium.OutputType
import org.openqa.selenium.support.ui.ExpectedConditions
import org.openqa.selenium.support.ui.WebDriverWait

import java.nio.file.Files
import java.nio.file.Paths

def takeScreenshot = { driver, path ->
  Files.write(Paths.get(path),
              driver.getScreenshotAs(OutputType.BYTES))
}

def browser = new Browser()

browser.go 'https://www.google.co.jp/'

assert browser.title == 'Google'

takeScreenshot(browser.driver, 'capture1.png')

browser.$('input[name="q"]').with {
  value('Cheese!')
  it << Keys.chord(Keys.ENTER)
  // firstElement().submit()  // SeleniumのWebElementを使う場合はこちら
}

(new WebDriverWait(browser.driver, 10)).until(ExpectedConditions.titleContains("Google 検索"))

assert browser.title == 'Cheese! - Google 検索'

takeScreenshot(browser.driver, 'capture2.png')

browser.quit()

スクリーンショットを撮るところ、普通に書いてしまいましたが、合ってる…?

なかなか簡潔に使えて良いと思いますー。

ScalaTest

ScalaTestに、Selenium向けのDSLがあるようです。

Using Selenium
http://www.scalatest.org/user_guide/using_selenium

自分はScalaTest派なので、ちょっと嬉しかったり。

sbtの設定は、このように。
build.sbt

name := "scalatest-selenium"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.5"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "2.2.4" % "test",
  "org.seleniumhq.selenium" % "selenium-java" % "2.45.0" % "test",
  "com.google.code.findbugs" % "jsr305" % "3.0.0" % "provided"
)

Firefoxを使うので、「selenium-firefox-driver」だけでもいいのかなぁと思いましたが、それだとうまくいかずドキュメント通りに「selenium-java」を足すことにしました…。

あと、JSR-305向けのfindbugsの依存関係があるのですが、これは以下のWarningを消すためです。

[warn] Class javax.annotation.Nullable not found - continuing with a stub.
[warn] one warning found

ScalaTestでのSeleniumの使い方ですが、各ブラウザに相当するトレイトをMix-inする簡易な方法と、WebBrowserトレイトをMix-inしてWebDriverのインスタンスはimplicitとして持つ方法があるようです。なお、各ブラウザに相当するトレイト(例えばFirefoxトレイト)は、WebBrowserトレイトのサブトレイトです。

では、最初のシナリオを書いてみます。
src/test/scala/org/littlewings/scalatest/ScalaTestSeleniumUnitSpec.scala

package org.littlewings.scalatest

import org.scalatest.{ BeforeAndAfterAll, FunSpec }
import org.scalatest.Matchers._
import org.scalatest.selenium.Firefox

import org.openqa.selenium.WebDriver
import org.openqa.selenium.support.ui.{ ExpectedCondition, WebDriverWait }

class ScalaTestSeleniumUnitSpec extends FunSpec
    with Firefox
    with BeforeAndAfterAll {
  describe("ScalaTest Selenium Unit Style Spec") {
    it("Google Search") {
      go to ("https://www.google.co.jp/")

      pageTitle should be ("Google")

      click on "q"
      enter("Cheese!")
      submit()

      (new WebDriverWait(webDriver, 10)).until(new ExpectedCondition[Boolean] {
        override def apply(d: WebDriver): Boolean =
          d.getTitle.toLowerCase.startsWith("cheese!")
      })

      pageTitle should be ("Cheese! - Google 検索")
    }
  }

  override def afterAll(): Unit = quit()
}

Firefoxを使うために、FirefoxトレイトをMix-inしています。BeforeAndAfterAllトレイトをMix-inしているのは、最後にquitメソッドを呼ぶためだけです。

続いて、スクリーンショットを撮る方。こちらは、WebDriverのインスタンスをimplicit valで定義しています。
src/test/scala/org/littlewings/scalatest/ScalaTestSeleniumScreenshotSpec.scala

package org.littlewings.scalatest

import org.scalatest.{ BeforeAndAfterAll, FunSpec }
import org.scalatest.Matchers._
import org.scalatest.selenium.WebBrowser

import org.openqa.selenium.WebDriver
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.support.ui.{ ExpectedConditions, WebDriverWait }

class ScalaTestSeleniumScreenshotSpec extends FunSpec
    with WebBrowser
    with BeforeAndAfterAll {
  implicit val webDriver: WebDriver = new FirefoxDriver

  describe("ScalaTest Selenium Using WebDriver Spec") {
    it("Take Screenshot") {
      setCaptureDir(".")

      go to ("https://www.google.co.jp/")

      pageTitle should be ("Google")

      capture to "capture1.png"

      click on "q"
      enter("Cheese!")
      submit()

      (new WebDriverWait(webDriver, 10))
        .until(ExpectedConditions.titleContains("Google 検索"))

      pageTitle should be ("Cheese! - Google 検索")

      capture to "capture2.png"
    }
  }

  override def afterAll(): Unit = webDriver.quit()
}

こちらでは、capture toでスクリーンショットを保存できるのですが、

      capture to "capture1.png"

保存先はデフォルトでは一時ディレクトリ(Linuxなら/tmp)だったりします。これは、setCaptureDirで変更することができます。

      setCaptureDir(".")

今回は、実行時のカレントディレクトリとしました。

なお、このScalaTestのSelenium DSLですが、各メソッド(例えばpageTitle)にimplicit parameterがいて、WebDriverのインスタンスを要求するようです。このあたりを少し楽にしてくれるのが、各ブラウザごとのトレイトみたいです。

久々にこういうのやると、面白いですね。

実際、自動化とかでお手軽に使うのならGebなのかなぁ。個人的にはScalaが好みではありますが。