かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その34 )( Geb でテストを作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その33 )( ESLint を導入する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Geb で入力画面1、入力画面2のテストを作成してみます。
    • Form に値を一括セット/検証する方法を調べていたら結構時間がかかってしまいました。

参照したサイト・書籍

  1. The Book Of Geb
    http://www.gebish.org/manual/current/

  2. Gebチートシート
    https://qiita.com/itagakishintaro/items/1fa06904bd0a6de73ee2

  3. Geb Advent Calendar 2016
    https://qiita.com/advent-calendar/2016/geb

  4. Geb 自分用メモ
    http://bufferings.hatenablog.com/entry/2015/06/11/010321

  5. Web画面自動テストフレームワークGeb」の紹介
    http://lab.astamuse.co.jp/entry/geb_test_01

  6. Gebチュートリアル
    https://www.atware.co.jp/blog/2015/9/12/geb

  7. Can selenium handle autocomplete?
    https://stackoverflow.com/questions/663034/can-selenium-handle-autocomplete

目次

  1. Firefox Quantum がインストールされていました
  2. FormModule クラスを作成する
  3. 入力画面1の InquiryInput01Page クラスを変更し、テストデータを用意する
  4. 入力画面2の InquiryInput02Page クラスを作成し、テストデータを用意する
  5. Geb のテストを作成する
  6. 続きます。。。

手順

Firefox Quantum がインストールされていました

Firefox を再起動したら Firefox Quantum になっていました。そういえば正式リリース日でしたね(2017/11/15 に書いています)。既存の Firefox とは別にインストールする必要があるのかな、と思っていたら自動でアップデートされていました。

f:id:ksby:20171115012802p:plain

サンプルで書いていた Geb のテストを試したら問題なく動くようなので、このまま続けます。

FormModule クラスを作成する

画面共通のボタンを定義したり、Form に値を一括セット/検証するメソッドを作成したいので、Geb の Module を継承したクラスを作成します。

src/test/groovy/geb の下に module パッケージを作成します。src/test/groovy/geb/module の下に FormModule.groovy を新規作成し、以下の内容を記述します。

package geb.module

import geb.Module
import org.openqa.selenium.WebElement

/**
 * フォーム共通の content や、テストに使用するメソッドを定義するクラス
 */
class FormModule extends Module {

    static content = {
        btnBack { $(".js-btn-back") }
        btnNext { $(".js-btn-next") }
    }

    /**
     * Form の入力項目に値を一括セットする
     * valueList は以下の形式の Map である
     * <pre>{@code
     * static initialValueList = [
     *      "#lastname"        : "",
     *      "#firstname"       : "",
     *      "#lastkana"        : "",
     *      "#firstkana"       : "",
     *      "input[name='sex']": null,
     *      "#age"             : "",
     *      "#job"             : ""
     * ]
     *}</pre>
     *
     * @param valueList セットするセレクタと値を記述した Map
     */
    void setValueList(valueList) {
        valueList.each {
            $(it.key).value(it.value)
        }
    }

    /**
     * セレクタに値がセットされているかを検証する
     * このメソッドは Spock の then, expect で使用する想定である
     *
     * @param valueList 検証するセレクタと値を記述した Map
     * @return true 固定
     */
    boolean assertValueList(valueList) {
        valueList.each { key, value ->
            if ($(key).first().attr("type") == "radio") {
                WebElement element = $(key).allElements().find { it.selected }
                if (element == null) {
                    assert null == value
                } else {
                    assert element.getAttribute("value") == value
                }
            } else {
                assert $(key).value() == value
            }
        }
        true
    }

}

入力画面1の InquiryInput01Page クラスを変更し、テストデータを用意する

入力画面1用に作成していた InquiryInput01Page クラスで FormModule を使用するようにし、かつテストで使用するデータも記述します。src/test/groovy/geb/page/inquiry/InquiryInput01Page.groovy を以下のように変更します。

package geb.page.inquiry

import geb.Page
import geb.module.FormModule

class InquiryInput01Page extends Page {

    static url = "/inquiry/input/01"
    static at = { title == "入力フォーム - 入力画面1" }
    static content = {
        form { module FormModule }
    }

    static initialValueList = [
            "#lastname"        : "",
            "#firstname"       : "",
            "#lastkana"        : "",
            "#firstkana"       : "",
            "input[name='sex']": null,
            "#age"             : "",
            "#job"             : ""
    ]

    static maxLengthValueList = [
            "#lastname" : "あ" * 20,
            "#firstname": "あ" * 20,
            "#lastkana" : "あ" * 20,
            "#firstkana": "あ" * 20,
            "#age"      : "9" * 3,
    ]

}
  • static content = { ... } から btnNext { $(".js-btn-next") } を削除し、form { module FormModule } を追加します。
  • static initialValueList = [ ... ] を追加します。
  • static maxLengthValueList = [ ... ] を追加します。

入力画面2の InquiryInput02Page クラスを作成し、テストデータを用意する

入力画面2用の InquiryInput02Page クラスを作成します。テストで使用するデータも記述します。

src/test/groovy/geb/page/inquiry の下に InquiryInput02Page.groovy を新規作成し、以下の内容を記述します。

package geb.page.inquiry

import geb.Page
import geb.module.FormModule

class InquiryInput02Page extends Page {

    static url = "/inquiry/input/01"
    static at = { title == "入力フォーム - 入力画面2" }
    static content = {
        form { module FormModule }
    }

    static initialValueList = [
            "#zipcode1": "",
            "#zipcode2": "",
            "#address" : "",
            "#tel1"    : "",
            "#tel2"    : "",
            "#tel3"    : "",
            "#email"   : "",
    ]

    static maxLengthValueList = [
            "#zipcode1": "x" * 3,
            "#zipcode2": "x" * 4,
            "#address" : "あ" * 256,
            "#tel1"    : "9" * 5,
            "#tel2"    : "9" * 4,
            "#tel3"    : "9" * 4,
            "#email"   : "x" * 256,
    ]

}

Geb のテストを作成する

入力画面1、入力画面2でできるテストを作成してみます。

src/test/groovy/geb/gebspec の下に inquiry パッケージを作成した後、inquiry パッケージの下に InquiryTestSpec.groovy を新規作成して以下の内容を記述します。

package geb.gebspec.inquiry

import geb.page.inquiry.InquiryInput01Page
import geb.page.inquiry.InquiryInput02Page
import geb.spock.GebSpec
import org.openqa.selenium.Keys
import org.openqa.selenium.WebElement
import spock.lang.Unroll

class InquiryTestSpec extends GebSpec {

    def "入力画面1の画面初期表示時に想定している値がセットされている"() {
        setup: "入力画面1を表示する"
        to InquiryInput01Page

        expect: "初期値が表示されている"
        form.assertValueList(initialValueList)
    }

    def "入力画面1の各入力項目に最大文字数を入力できる"() {
        setup: "入力画面1を表示し入力項目に最大文字数の文字を入力する"
        to InquiryInput01Page
        form.setValueList(maxLengthValueList)

        expect: "入力した最大文字数の文字が入力されている"
        form.assertValueList(maxLengthValueList)
    }

    def "入力画面1に入力後、入力画面2へ遷移→入力画面1へ戻ると入力した値が表示される"() {
        given: "入力画面1を表示する"
        to InquiryInput01Page

        when: "最大文字数の文字を入力して次へボタンをクリックする"
        form.setValueList(maxLengthValueList)
        $("#inquiryInput01Form").sex = "1"
        form.btnNext.click(InquiryInput02Page)

        then: "入力画面2へ遷移し初期値が表示されている"
        form.assertValueList(initialValueList)

        and: "戻るボタンをクリックする"
        form.btnBack.click(InquiryInput01Page)

        then: "入力した最大文字数の文字が入力されている"
        form.assertValueList(maxLengthValueList)
        $("#inquiryInput01Form").sex == "1"
    }

    def "入力画面2の各入力項目に最大文字数を入力できる"() {
        setup: "入力画面1を表示して最大文字数の文字を入力してから次へボタンをクリックする"
        to InquiryInput01Page
        form.setValueList(maxLengthValueList)
        $("#inquiryInput01Form").sex = "1"
        form.btnNext.click(InquiryInput02Page)

        and: "入力画面2で最大文字数の文字を入力する"
        form.setValueList(maxLengthValueList)

        expect: "入力した最大文字数の文字が入力されている"
        form.assertValueList(maxLengthValueList)
    }

    def "入力画面2で郵便番号を入力してautocompleteで表示された住所を選択する"() {
        given: "入力画面1から入力画面2へ遷移する"
        to InquiryInput01Page
        form.setValueList(maxLengthValueList)
        $("#inquiryInput01Form").sex = "1"
        form.btnNext.click(InquiryInput02Page)

        when: "郵便番号を入力する"
        $("#inquiryInput02Form").zipcode1 = "100"
        $("#inquiryInput02Form").zipcode2 = "0005" << Keys.TAB

        and: "autocomplete のドロップダウンメニューが表示されたら最初の選択肢を選択する"
        waitFor(5) { $(".ui-autocomplete .ui-menu-item") }
        List<WebElement> elementList = $(".ui-autocomplete .ui-menu-item > div").allElements()
        elementList.first().click()

        then: "住所にクリックした選択肢がセットされている"
        $("#inquiryInput02Form").address == "東京都千代田区丸の内"
    }

    @Unroll
    def "入力画面2の電話番号とメールアドレスの組み合わせテスト(#tel1,#tel2,#tel3,#email)"() {
        given: "入力画面1から入力画面2へ遷移する"
        to InquiryInput01Page
        form.setValueList(maxLengthValueList)
        $("#inquiryInput01Form").sex = "1"
        form.btnNext.click(InquiryInput02Page)

        when: "電話番号と郵便番号を入力する"
        $("#inquiryInput02Form").tel1 = tel1 << Keys.TAB
        $("#inquiryInput02Form").tel2 = tel2 << Keys.TAB
        $("#inquiryInput02Form").tel3 = tel3 << Keys.TAB
        $("#inquiryInput02Form").email = email << Keys.TAB

        then: "エラーメッセージの表示状況をチェックする"
        $("#form-group-tel .js-errmsg").text() == telErrMsg
        $("#form-group-email .js-errmsg").text() == emailErrMsg

        where:
        tel1 | tel2   | tel3   | email                 || telErrMsg                            | emailErrMsg
        "03" | "1234" | "5678" | "tanaka@sample.co.jp" || ""                                   | ""
        "03" | "1234" | "5678" | ""                    || ""                                   | ""
        ""   | ""     | ""     | "tanaka@sample.co.jp" || ""                                   | ""
        ""   | ""     | ""     | ""                    || "電話番号とメールアドレスのいずれか一方を入力してください"       | "電話番号とメールアドレスのいずれか一方を入力してください"
        "3"  | "1234" | "5678" | ""                    || "市外局番の先頭には 0 の数字を入力してください"           | ""
        "03" | "123"  | "5678" | ""                    || "市外局番+市内局番の組み合わせが数字6桁になるように入力してください" | ""
        "03" | "1234" | "567"  | ""                    || "加入者番号には4桁の数字を入力してください"              | ""
    }

}

テストを書いてみて思ったのは、以下の点でした。

  • InquiryInput01Page にも InquiryInput02Page にも static content = { form { module FormModule } } と書いており、テストは form.~ から書いて問題なく動作するのかな?と不思議に思っていましたが、to ...form.btnNext.click(...Page) を呼び出して画面遷移した後の Page クラスの form を見てくれるようです。
  • 値をセットするだけなら不要ですが、Javascriptblur イベントを発生させたい場合には << Keys.TAB が必須でした。
  • autocomplete のテストは時間がかかりましたが、それ以外は MockMvc と比較するとテストが書きやすいです。ただし実行に時間がかかるので、何でも Geb でテストを作成するという訳にはいかなそうです。

作成したテストを実行してみます。Tomcat を起動した後、テストを実行します。

f:id:ksby:20171117002300p:plain

テストは全て成功しました。最初のテストが 10数秒、それ以降のテストが2~4秒程度かかるようです。ブラウザを起動してテストをするので、やっぱり結構時間がかかりますね。

Headless Chrome に切り替えてみます。src/test/resources/GebConfig.groovy を以下のように変更します。

driver = {
//    FirefoxOptions firefoxOptions = new FirefoxOptions()
//    firefoxOptions.setHeadless(true)
//    new FirefoxDriver(firefoxOptions)
    ChromeOptions chromeOptions = new ChromeOptions()
    chromeOptions.setHeadless(true)
    new ChromeDriver(chromeOptions)
}

テストを実行します。

f:id:ksby:20171117003317p:plain

Firefox headless モードの時と同様にテストは全て成功しました。Firefox headless モードと比較すると最初のテストは数秒速いですが、その後のテストが 0.5~2秒くらい遅い気がします。

最後に HtmlUnit に切り替えてみます。src/test/resources/GebConfig.groovy を以下のように変更します。

driver = {
//    FirefoxOptions firefoxOptions = new FirefoxOptions()
//    firefoxOptions.setHeadless(true)
//    new FirefoxDriver(firefoxOptions)
    new HtmlUnitDriver(true)
}

テストを実行します。

f:id:ksby:20171117004939p:plain

なぜか全部失敗しましたね。。。 java.lang.NoSuchMethodError: com.gargoylesoftware.htmlunit.html.DomElement.getScriptableObject()Ljava/lang/Object; というエラーが出ています。

続きます。。。

HtmlUnit で失敗する原因を調べます。また Geb でもう少しいろいろ試してみます。

履歴

2017/11/17
初版発行。