かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は 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
初版発行。

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

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その32 )( npm の admin-lte package から jQuery がなくなっていたので対応する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Geb のテストを書き進める前に ESLint の導入の仕方を知りたくなったので、先に導入します。
    • 以下の内容で導入します。
      • eslint-config-airbnb-base を利用します。
      • jQuery 用の設定に変更します。

参照したサイト・書籍

  1. ESLint - Pluggable JavaScript linter
    https://eslint.org/

  2. 愚直にESLintを導入した話
    http://tech.mercari.com/entry/2017/07/31/170125

  3. eslint-config-airbnb-base
    https://www.npmjs.com/package/eslint-config-airbnb-base

  4. webpack から ESLint を使う設定
    https://www.pupha.net/archives/3289/

  5. eslint-loader
    https://www.npmjs.com/package/eslint-loader

  6. ESLintでReactとES2015の構文チェック(eslint-config-airbnb)
    http://dackdive.hateblo.jp/entry/2016/05/13/094000

  7. ESLint - Rules
    https://eslint.org/docs/rules/

  8. Can I use eslint-config-airbnb without eslint-plugin-react?
    https://github.com/airbnb/javascript/issues/451

  9. ESLint dollar($) is not defined. (no-undef)
    https://stackoverflow.com/questions/39510736/eslint-dollar-is-not-defined-no-undef

  10. Airbnb JavaScript Style Guide
    https://github.com/airbnb/javascript

目次

  1. ESLint をインストールする
  2. eslint --init を実行する
  3. ECMAScript 6 用の Rule を含まない airbnb-baseairbnb-base/legacy に変更する
  4. webpack から eslint が実行されるよう設定する
  5. 動作確認
  6. インデントと改行コードと文字列のクォーテーションを airbnb-base のものではなく独自ルールに変更する
  7. 修正を進める前に IntelliJ IDEA の ESLint の機能を有効にする
  8. 多めに出力されているメッセージに対応する
    1. jquery.autoKana.js をチェック対象から外す
    2. app.js を削除する
    3. '$' is not defined
    4. Unexpected unnamed function
    5. All 'var' declarations must be at the top of the function scope
    6. Expected a newline after '('
    7. Strings must use doublequote
    8. 'errmsg' used outside of binding context
    9. 'event' is defined but never used
    10. Missing semicolon
    11. Unexpected use of 'location'
  9. 各ファイル毎の出力されているメッセージに対応する
    1. src/main/assets/js/inquiry/input01.js
    2. src/main/assets/js/inquiry/input02.js
    3. src/main/assets/js/lib/class/Form.js
    4. src/main/assets/js/lib/util/converter.js
  10. 最後に

手順

ESLint をインストールする

npm install --save-dev eslint コマンドを実行してインストールします。

f:id:ksby:20171103080757p:plain f:id:ksby:20171103080915p:plain

eslint --init を実行する

git コマンドを実行するために使用している git-cmd.exe を起動してから ./node_modules/.bin/eslint --init コマンドを実行します。CUI で4つ質問事項が表示されますので、下の画像の上の方に表示されている内容を回答します。

  • How would you like to configure ESLint? --> Use a popular style guide
  • Which style guide do you want to follow? --> Airbnb
  • Do you use React? --> No
  • What format do you want your config file to be in? --> JavaScript

f:id:ksby:20171103081501p:plain

https://eslint.org/docs/user-guide/getting-started に書かれていたコマンドが ./node_modules/.bin/eslint --init だったので git-cmd.exe から実行しましたが、/ --> \ に変更してコマンドプロンプトから実行しても同じように動作します。

質問に回答すると npm で eslint-config-airbnb-base と eslint-plugin-import の package がインストールされて、package.json が以下のように変更され、

    ..........
    "cssnano": "^3.10.0",
    "eslint": "^4.10.0",
    "eslint-config-airbnb-base": "^12.1.0",
    "eslint-plugin-import": "^2.8.0",
    "http-proxy-middleware": "^0.17.4",
    ..........
  • 以下の2行が追加されていました。
    • "eslint-config-airbnb-base": "^12.1.0"
    • "eslint-plugin-import": "^2.8.0"

プロジェクトのルート直下に以下の内容の .eslintrc.js が生成されます。

module.exports = {
    "extends": "airbnb-base"
};

ECMAScript 6 用の Rule を含まない airbnb-baseairbnb-base/legacy に変更する

node_modules/eslint-config-airbnb-base/index.js は ECMAScript 6 用の Rule が記述されている './rules/es6' が含まれていますが、

module.exports = {
  extends: [
    './rules/best-practices',
    './rules/errors',
    './rules/node',
    './rules/style',
    './rules/variables',
    './rules/es6',
    './rules/imports',
  ].map(require.resolve),
  parserOptions: {
    ecmaVersion: 2017,
    sourceType: 'module',
    ecmaFeatures: {
      experimentalObjectRestSpread: true,
    },
  },
  rules: {
    strict: 'error',
  },
};

'./rules/es6' を含まない node_modules/eslint-config-airbnb-base/legacy.js というファイルもあります。

module.exports = {
  extends: [
    './rules/best-practices',
    './rules/errors',
    './rules/node',
    './rules/style',
    './rules/variables'
  ].map(require.resolve),
  env: {
    browser: true,
    node: true,
    amd: false,
    mocha: false,
    jasmine: false
  },
  rules: {
    'comma-dangle': ['error', 'never'],
    'prefer-numeric-literals': 'off',
    'no-restricted-properties': ['error', {
      object: 'arguments',
      property: 'callee',
      message: 'arguments.callee is deprecated',
    }, {
      property: '__defineGetter__',
      message: 'Please use Object.defineProperty instead.',
    }, {
      property: '__defineSetter__',
      message: 'Please use Object.defineProperty instead.',
    }],
  }
};

今回 ECMAScript 6 は使用していないので、legacy.js を使用するよう .eslintrc.js を以下のように変更します。

module.exports = {
    "extends": "airbnb-base/legacy"
};
  • airbnb-baseairbnb-base/legacy に変更します。

webpack から eslint が実行されるよう設定する

npm install --save-dev eslint-loader コマンドを実行して eslint-loader をインストールします。

f:id:ksby:20171103085120p:plain

webpack.config.js を以下のように変更します。

module.exports = {
    ..........
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "eslint-loader"
            }
        ]
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ]
};
  • module: { ... } を追加します。

動作確認

npm run spingboot コマンドを実行して webpack を実行してみると、なんか大量にメッセージが出力されました。。。

f:id:ksby:20171104034328p:plain

改行コードとインデントと文字列のクォーテーションで結構メッセージが表示されているようなので、まずは eslint のルールを今 js ファイルを書いているルール(4インデント+CRLF+ダブルクォーテーション)に変更できないか調べてみます。

インデントと改行コードと文字列のクォーテーションを airbnb-base のものではなく独自ルールに変更する

カスタマイズは .eslintrc.js に "rules": { ... } で記述すればできるようなので、.eslintrc.js を以下のように変更します。

module.exports = {
    "extends": "airbnb-base",
    "rules": {
        // this option sets a specific tab width for your code
        // http://eslint.org/docs/rules/indent
        indent: ['error', 4, {
            SwitchCase: 1,
            VariableDeclarator: 1,
            outerIIFEBody: 1,
            // MemberExpression: null,
            FunctionDeclaration: {
                parameters: 1,
                body: 1
            },
            FunctionExpression: {
                parameters: 1,
                body: 1
            },
            CallExpression: {
                arguments: 1
            },
            ArrayExpression: 1,
            ObjectExpression: 1,
            ImportDeclaration: 1,
            flatTernaryExpressions: false,
            ignoredNodes: ['JSXElement', 'JSXElement *']
        }],

        // disallow mixed 'LF' and 'CRLF' as linebreaks
        // http://eslint.org/docs/rules/linebreak-style
        'linebreak-style': ['error', 'windows'],

        // specify whether double or single quotes should be used
        quotes: ['error', 'double', { avoidEscape: true }]
    }
};
  • 改行コードとインデントと文字列のクォーテーションの設定は全て node_modules/eslint-config-airbnb-base/rules/style.js の中に書かれていたので、そこからコピーして設定を変更します。
  • indent の設定は 2-space indent 以外にもいろいろ設定が入っていたので、コピーして 24 のみ変更します。
  • linebreak-style の設定を unixwindows に変更します。
  • quotes の設定を singledouble に変更します。

再度 npm run spingboot コマンドを実行すると改行コードとインデントと文字列のクォーテーションではメッセージが出なくなりました。

f:id:ksby:20171104040602p:plain

修正を進める前に IntelliJ IDEA の ESLint の機能を有効にする

IntelliJ IDEA には ESLint を利用してソースコードをチェックする機能があるので、それを有効にします。

メインメニューから「File」-「Settings...」を選択します。「Settings」ダイアログを表示されるので、画面左上の検索フィールドに "eslint" と入力します。

ESLint の設定画面が表示されるので、以下の画面のように設定します。

f:id:ksby:20171104134640p:plain

  • 「Enable」をチェックします。
  • 「Node interpreter」に "node.exe" のパスを設定します。
  • 「Configuration file」はデフォルトの「Automatic search」を選択したままです。
  • 「Additional rules directory」「Extra eslint options」は何も設定しません。

「OK」ボタンを押してダイアログを閉じた後 js のソースコードを表示すると、以下の画像のように ESLint のチェックに引っかかった行の右側に赤線が表示され、マウスオーバーすると ESLint のメッセージが表示されます。

f:id:ksby:20171104135904p:plain

またエディタ上で右クリックしてコンテキストメニューを表示した後「Analyze」-「Inspect Code...」を選択し、

f:id:ksby:20171104140142p:plain

「Specify Inspection Scope」ダイアログが表示されたらそのまま「OK」を押すと、

f:id:ksby:20171104140341p:plain

画面下部に Inspection の結果が表示されます。この中に「Code quality tools」という項目があり、

f:id:ksby:20171104140639p:plain

展開すると ESLint のメッセージと該当箇所が表示されます。

f:id:ksby:20171104140943p:plain

多めに出力されているメッセージに対応する

ざっと見てまだ多めに出ているメッセージから対応します。

jquery.autoKana.js をチェック対象から外す

jquery.autoKana.js がチェックされてメッセージが出ていましたので、チェック対象から外します。webpack.config.js を以下のように変更します。

    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: [
                    /node_modules/,
                    /jquery.autoKana.js$/
                ],
                loader: "eslint-loader"
            }
        ]
    },
  • exclude に /jquery.autoKana.js$/ を追加します。

app.js を削除する

src/main/assets/js/app.js もチェックされていましたが、このファイルは最初に作成したサンプルでもう不要なので削除します。

app.js を削除した後、webpack.config.js を以下のように変更します。

module.exports = {
    entry: {
        "js/inquiry/input01": ["./src/main/assets/js/inquiry/input01.js"],
        "js/inquiry/input02": ["./src/main/assets/js/inquiry/input02.js"],
        "js/inquiry/input03": ["./src/main/assets/js/inquiry/input03.js"],
        "js/inquiry/confirm": ["./src/main/assets/js/inquiry/confirm.js"]
    },
  • "js/app": ["./src/main/assets/js/app.js"] を削除します。

'$' is not defined

ESLint dollar($) is not defined. (no-undef) を見ると "jquery": true を定義すればチェックされないようになるようです。.eslintrc.js を以下のように変更します。

module.exports = {
    "extends": "airbnb-base/legacy",
    "env": {
        "jquery": true
    },
    ..........
  • "env": { "jquery": true } を追加します。

Unexpected unnamed function

無名関数は多用するので、この Rule は無効にします。.eslintrc.js を以下のように変更します。

    "rules": {
        // require function expressions to have a name
        // http://eslint.org/docs/rules/func-names
        'func-names': 'off',
  • 'func-names': 'off' を追加します。

All 'var' declarations must be at the top of the function scope

変数宣言は関数スコープ内の最初に行うこと、というメッセージのようですが、エラーが出ている原因は require("vendor/autokana/jquery.autoKana.js");require("jquery-ui/ui/widgets/autocomplete.js"); を上に書いているので、その下の var で宣言している部分でチェックに引っかかっているためのようです。

var より require を上にまとめておきたいので、この Rule は無効にします。.eslintrc.js を以下のように変更します。

    "rules": {
        // requires to declare all vars on top of their containing scope
        'vars-on-top': 'off',
  • 'vars-on-top': 'off' を追加します。

Expected a newline after '('

https://eslint.org/docs/rules/function-paren-newline を見ると、( の後に続けて引数を記述するのではなく、一旦改行をして次の行から書くこと、というメッセージのようです。なんか縦に長くなってソースが見にくくなる気しかしないので、これは無効にします。

    "rules": {
        ..........

        // require function expressions to have a name
        // http://eslint.org/docs/rules/func-names
        'func-names': 'off',

        // enforce consistent line breaks inside function parentheses
        // https://eslint.org/docs/rules/function-paren-newline
        'function-paren-newline': 'off',

        // this option sets a specific tab width for your code
        // http://eslint.org/docs/rules/indent
        indent: ['error', 4, {
  • 'function-paren-newline': 'off' を追加します。

Strings must use doublequote

文字列のクォーテーションはダブルクォーテーションを使用するルールを指定しましたが、シングルクォーテーションになっているところがありました。こちらは全てダブルクォーテーションに修正します。

'errmsg' used outside of binding context

src/main/assets/js/inquiry/input02.js で errmsg 変数を block スコープ毎に var で宣言していたので引っかかっているようです。関数の最初で var で宣言するように修正します。

'event' is defined but never used

イベントハンドラの無名関数の引数に使用している/いないに関わらず event という引数を書いていたので、そこで引っかかっているようです。使用されていない event 引数は削除します。

Missing semicolon

単純に末尾のセミコロンのつけ忘れです。修正します。

Unexpected use of 'location'

node_modules/eslint-config-airbnb-base/rules/variables.js を見ると以下のように定義されており、

const restrictedGlobals = require('eslint-restricted-globals');

module.exports = {
  rules: {
    ..........

    // disallow specific globals
    'no-restricted-globals': ['error', 'isFinite', 'isNaN'].concat(restrictedGlobals),

node_modules/eslint-restricted-globals/index.js には以下のように定義されていました。

module.exports = [
    ..........
    'length',
    'location',
    'locationbar',

locationグローバル変数として使用すると no-restricted-globals の Rule に引っかかりエラーになるようです。

ソースコードを修正して対応します。locationwindow.location に変更します。window は node_modules/eslint-restricted-globals/index.js に定義されておらず、グローバル変数として利用しても Rule に引っかかりません。

各ファイル毎の出力されているメッセージに対応する

src/main/assets/js/inquiry/input01.js

  • 'idList' is assigned a value but never used は宣言した変数を関数内で使用していないことが原因なので、変数を宣言している行を削除します。
  • 'validator' is already declared in the upper scope はソースの上で宣言している var validator = require("lib/util/validator.js"); と同じ validator という変数を使用していたので、validatorvalidateFunction に変更します。

src/main/assets/js/inquiry/input02.js

  • 'validator' is already declared in the upper scope は input01.js と同様に validatorvalidateFunction に変更します。
  • A space is required after '{'A space is required before '}'IntelliJ IDEA のフォーマッターがスペースを入れないようになっているので、この Rule を無効に変更します。.eslintrc.js を以下のように変更します。
    "rules": {
        ..........

        // require padding inside curly braces
        'object-curly-spacing': ['error', 'never']
    }
  • 'event' is already declared in the upper scopeevente に変更します。

src/main/assets/js/lib/class/Form.js

  • 'Form' was used before it was definedmodule.exports = Form; の後に function Form(idList) { ... } の記述があるために出力されていますが、module.exports の記述を先頭にしたいので .eslintrc.js に no-use-before-define の設定を node_modules/eslint-config-airbnb-base/rules/variables.js からコピーして functions: truefunctions: false に変更します。
    "rules": {
        ..........

        // disallow use of variables before they are defined
        'no-use-before-define': ['error', { functions: false, classes: true, variables: true }]
    }
  • Line 21 exceeds the maximum line length of 100 は1行の最大値を 120 に変更したいので、.eslintrc.js に max-len の設定を node_modules/eslint-config-airbnb-base/rules/style.js からコピーして 100120 に変更します。
    "rules": {
        ..........

        // specify the maximum length of a line in your program
        // http://eslint.org/docs/rules/max-len
        'max-len': ['error', 120, 2, {
            ignoreUrls: true,
            ignoreComments: false,
            ignoreRegExpLiterals: true,
            ignoreStrings: true,
            ignoreTemplateLiterals: true
        }]
    }
  • Assignment to property of function parameter 'form' は関数の引数で渡された変数を変更しようとしているから出ているメッセージらしいのですが、form を引数に渡さないようにする方法が今だによく分かっていないので、ソースの先頭に /* eslint-disable no-param-reassign,lines-around-directive */ を記述して Form.js だけこの Rule を無効にします。

src/main/assets/js/lib/util/converter.js

  • Multiple spaces found before ... はソースの右側にスペース複数を入れてからコメントを書いていたためでした。コメントは移動してコメントだけの行になるようにします。

以上で全ての対応が完了です。

最後に

動きはしていたので今回はリンターを導入してもそんなに指摘は受けないかな、と思っていましたが、いつものように結構エラーが出ました。。。 ESLint は eslint-config-airbnb や eslint-config-airbnb-base といった使える設定が公開されているし、導入例の記事もいろいろ公開されていたので導入しやすかったです。また Airbnb JavaScript Style Guide は有名なようなので、普段 Javascript を実装しない自分としては読んでおきたいと思います。

また今回の調査をしていた時に prettier という Javascript のフォーマッターの記事を見かけました。Using External tools: ESLint autofix, React Native and Prettier を見ると IntelliJ IDEA でもサポートされているようなので、時間があれば見てみたいと思います。

最後に .eslintrc.js の最終版を載せておきます。

module.exports = {
    "extends": "airbnb-base/legacy",
    "env": {
        "browser": true,
        "jquery": true
    },
    "rules": {
        // requires to declare all vars on top of their containing scope
        'vars-on-top': 'off',

        // require function expressions to have a name
        // http://eslint.org/docs/rules/func-names
        'func-names': 'off',

        // enforce consistent line breaks inside function parentheses
        // https://eslint.org/docs/rules/function-paren-newline
        'function-paren-newline': 'off',

        // this option sets a specific tab width for your code
        // http://eslint.org/docs/rules/indent
        indent: ['error', 4, {
            SwitchCase: 1,
            VariableDeclarator: 1,
            outerIIFEBody: 1,
            // MemberExpression: null,
            FunctionDeclaration: {
                parameters: 1,
                body: 1
            },
            FunctionExpression: {
                parameters: 1,
                body: 1
            },
            CallExpression: {
                arguments: 1
            },
            ArrayExpression: 1,
            ObjectExpression: 1,
            ImportDeclaration: 1,
            flatTernaryExpressions: false,
            ignoredNodes: ['JSXElement', 'JSXElement *']
        }],

        // disallow mixed 'LF' and 'CRLF' as linebreaks
        // http://eslint.org/docs/rules/linebreak-style
        'linebreak-style': ['error', 'windows'],

        // specify whether double or single quotes should be used
        quotes: ['error', 'double', { avoidEscape: true }],

        // require padding inside curly braces
        'object-curly-spacing': ['error', 'never'],

        // disallow use of variables before they are defined
        'no-use-before-define': ['error', { functions: false, classes: true, variables: true }],

        // specify the maximum length of a line in your program
        // http://eslint.org/docs/rules/max-len
        'max-len': ['error', 120, 2, {
            ignoreUrls: true,
            ignoreComments: false,
            ignoreRegExpLiterals: true,
            ignoreStrings: true,
            ignoreTemplateLiterals: true
        }]
    }
};

履歴

2017/11/05
初版発行。
2017/11/06
* "browser": true, は node_modules/eslint-config-airbnb-base/legacy.js で設定済なので削除しました。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( webpack で jQuery だけバンドルしないで外部ファイルを利用するには? )

概要

記事一覧はこちらです。

既存のページで既に jQuery を使用しており、そこに webpack でバンドルした js ファイルを導入したい場合に、jQuery だけバンドルしないで外部ファイルの jQuery を利用する方法が知りたくなったので調べてみます。

参照したサイト・書籍

  1. webpackでCDNから取ってきたりした外部のjQueryなどを利用する方法
    http://frontend-takuyan.hateblo.jp/entry/jquery-in-webpack

  2. webpack - Externals
    https://webpack.js.org/configuration/externals/

目次

  1. webpack.config.js を変更する
  2. 動作確認

本文

webpack.config.js を変更する

webpackでCDNから取ってきたりした外部のjQueryなどを利用する方法 の記事にずばりその通りの内容が記載されていました。webpack.config.js に以下の記述を追加すればいいそうです。

var webpack = require('webpack');

module.exports = {
    entry: {
        "js/app": ["./src/main/assets/js/app.js"],
        "js/inquiry/input01": ["./src/main/assets/js/inquiry/input01.js"],
        "js/inquiry/input02": ["./src/main/assets/js/inquiry/input02.js"],
        "js/inquiry/input03": ["./src/main/assets/js/inquiry/input03.js"],
        "js/inquiry/confirm": ["./src/main/assets/js/inquiry/confirm.js"]
    },
    output: {
        path: __dirname + "/src/main/resources/static",
        publicPath: "/",
        filename: "[name].js"
    },
    resolve: {
        modules: [
            "node_modules",
            "src/main/assets/js"
        ],
        alias: {
            jquery: "jquery"
        }
    },
    externals: {
        jquery: "jQuery"
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ]
};
  • externals: { jquery: "jQuery" } を追加します。ただし "jquery" ではなく、必ず "jQuery" と記述する必要があります。"jquery" だと正常に動作しませんでした。

動作確認

src/main/resources/static/vendor の下に node_modules/jquery/dist/jquery.min.js をコピーします。

src/main/resources/templates/web/inquiry/input01.html, input02.html を以下のように変更します。

<!-- REQUIRED JS SCRIPTS -->
<script src="/vendor/jquery.min.js"></script>
<script src="/js/inquiry/input01.js"></script>

</body>
</html>
  • 各画面用の js ファイルの上に <script src="/vendor/jquery.min.js"></script> を追加します。

コマンドプロンプトを起動して npm run springboot コマンドを実行し、Tomcat も起動します。

http://localhost:9080/inquiry/input/01/ にアクセスすると入力画面1が表示されます。

f:id:ksby:20171103000442p:plain

「次へ」ボタンをクリックすると入力チェックのエラーメッセージが表示されます。Javascript の処理が問題なく動作していますね。

f:id:ksby:20171103000859p:plain

autokana も動作しており、またデータを全て入力すると入力チェックOKの状態になります。

f:id:ksby:20171103001233p:plain

「次へ」ボタンをクリックして入力画面2へ遷移した後、郵便番号を入力すると、ajax でデータを取得して autocomplete で住所の選択肢も表示されます。

f:id:ksby:20171103002805p:plain

動作も問題なさそうです。

履歴

2017/11/03
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その32 )( npm の admin-lte package から jQuery がなくなっていたので対応する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その31 )( テスト対象のブラウザに Headless Chrome と HtmlUnit を追加する+Chrome, Firefox, HtmlUnit で連続テストする gradle タスクを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 今回作成しているプロジェクトは git clone してコマンドをいくつか実行するだけで利用可能になるようにしたつもりでいたのですが、試してみたところ admin-lte package から jQuery がなくなっており、エラーが出て動作させることができませんでした。動作できるように修正します。

参照したサイト・書籍

目次

  1. git clone から動作させようとしてエラーが出るところまでやってみる
    1. git clone する
    2. IntelliJ IDEA で boot-npm-geb-sample プロジェクトを開く
    3. npm install コマンドを実行する
    4. clean タスク → Rebuild Project → build タスクを実行する
  2. エラーの原因を修正する

手順

git clone から動作させようとしてエラーが出るところまでやってみる

git clone する

C:\project-test フォルダを作成して、この下に git clone します。

コマンドプロンプトを起動して C:\project-test フォルダに移動した後、git clone -b feature/1-issue https://github.com/ksby/ksbysample-boot-miscellaneous.git コマンドを実行します。

f:id:ksby:20171101002751p:plain

C:\project-test の下に ksbysample-boot-miscellaneous フォルダが作成され、その下に boot-npm-geb-sample プロジェクトが出来ます。

f:id:ksby:20171101003016p:plain

IntelliJ IDEA で boot-npm-geb-sample プロジェクトを開く

IntelliJ IDEA の「Welcome to IntelliJ IDEA」ダイアログを表示した後、「Open」ボタンをクリックします。

f:id:ksby:20171101003534p:plain

「Open File or Project」ダイアログが表示されますので、C:\project-test\ksbysample-boot-miscellaneous\boot-npm-geb-sample フォルダを選択して「OK」ボタンをクリックします。

f:id:ksby:20171101003756p:plain

「Import Project from Gradle」ダイアログが表示されますので、「Create directories for empty content roots automatically」をチェックした後「OK」ボタンをクリックします。

f:id:ksby:20171101004047p:plain

IntelliJ IDEA のメイン画面が開きますので、画面右下の処理中を示すプログレスバーが消えるまで待ちます。

処理が終了すると画面右側の Gradle projects が下の画像のようになります。

f:id:ksby:20171101004723p:plain

npm install コマンドを実行する

コマンドプロンプトで C:\project-test\ksbysample-boot-miscellaneous\boot-npm-geb-sample へ移動した後、npm install コマンドを実行します。

f:id:ksby:20171101005601p:plain

次に npm run springboot コマンドを実行します。。。が、Module not found: Error: Can't resolve 'admin-lte/plugins/jQuery/jquery-2.2.3.min.js' in ... というエラーメッセージが出力されました。なぜか jquery のファイルがないようです。。。

f:id:ksby:20171101005809p:plain f:id:ksby:20171101010017p:plain f:id:ksby:20171101010200p:plain f:id:ksby:20171101010442p:plain f:id:ksby:20171101010635p:plain f:id:ksby:20171101010735p:plain

clean タスク → Rebuild Project → build タスクを実行する

IntelliJ IDEA から clean タスク → Rebuild Project → build タスクを実行してみると、こちらは BUILD SUCCESSFUL のメッセージが出力されて正常に終了しました。

f:id:ksby:20171101011530p:plain

エラーの原因を修正する

以前は node_modules/admin-lte/plugins/jQuery/jquery-2.2.3.min.js というファイルが存在したのですが、node_modules/admin-lte/plugins の下を見ると jQuery フォルダ自体がなくなっていました。。。 admin-ltejQuery を同梱しなくなったんですね。

f:id:ksby:20171101014925p:plain

ということであれば普通に npm で jQuery をインストールして、そちらを使うように変更します。

まずは npm install --save jquery コマンドを実行して jQuery をインストールします。

f:id:ksby:20171101014758p:plain

次に webpack.config.js を以下のように変更します。

var webpack = require('webpack');

module.exports = {
    entry: {
        "js/app": ["./src/main/assets/js/app.js"],
        "js/inquiry/input01": ["./src/main/assets/js/inquiry/input01.js"],
        "js/inquiry/input02": ["./src/main/assets/js/inquiry/input02.js"],
        "js/inquiry/input03": ["./src/main/assets/js/inquiry/input03.js"],
        "js/inquiry/confirm": ["./src/main/assets/js/inquiry/confirm.js"]
    },
    output: {
        path: __dirname + "/src/main/resources/static",
        publicPath: "/",
        filename: "[name].js"
    },
    resolve: {
        modules: [
            "node_modules",
            "src/main/assets/js"
        ],
        alias: {
            jquery: "jquery"
        }
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ]
};
  • "admin-lte/plugins/jQuery/jquery-2.2.3.min.js""jquery" に変更します(全部で3ヶ所)。

これで変更は完了です。再び npm run springboot コマンドを実行してみると、今度はエラーメッセージは出ませんでした(下の方に白い文字で出ているメッセージは Tomcat が起動していないことによるものです)。

f:id:ksby:20171101015521p:plain

Tomcat を起動して。。。と思ったら spring.profiles.active を指定していないことによるエラーが出ました。

f:id:ksby:20171101015932p:plain

IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択し、「Run/Debug Configurations」ダイアログが表示されたら、画面左側の一覧から「Spring Boot」-「Application」を選択して、画面右側の「VM options」に -Dspring.profiles.active=develop -Dfile.encoding=UTF-8 を入力後「OK」ボタンをクリックします。

f:id:ksby:20171101020810p:plain

再度 Tomcat を起動したら、今度は問題なく起動しました。

f:id:ksby:20171101021041p:plain

ブラウザで http://localhost:9080/inquiry/input/01/ にアクセスすると入力画面1も表示されます。

f:id:ksby:20171101021221p:plain

何も入力せずに「次へ」ボタンを押すと入力エラーのある入力項目にエラーメッセージが表示されますので、Javascript も正常に動作しているようです。

f:id:ksby:20171101021447p:plain

ちょっとつまずきましたが、以前と比較すると試せるようになるまで全然簡単になりました。Edit Configuration の設定も何か簡単に出来る方法があるといいのですが。。。と思い Web で検索すると以下の記事を見つけました。これまで使用したことがありませんでしたが、share チェックボックスをチェックすればよいようです。そのうち試してみたいと思います。

How do I share IntelliJ Run/Debug configurations between projects? https://stackoverflow.com/questions/24642147/how-do-i-share-intellij-run-debug-configurations-between-projects

履歴

2017/11/01
初版発行。

Java SE を 8u144 → 8u152 へバージョンアップ

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

Java SE を 8u144 → 8u152 へバージョンアップする

  1. OracleJava SE Downloads を見ると 8u152 がダウンロードできるようになっていました。以下のページに説明があります。

    8u152 へバージョンアップします。

  2. jdk-8u152-windows-x64.exe をダウンロードして C:\Java\jdk1.8.0_152 へインストールした後、環境変数 JAVA_HOME のパスを C:\Java\jdk1.8.0_152 へ変更します。

    コマンドプロンプトから java -version を実行し、1.8.0_152 に変更されていることを確認します。

    f:id:ksby:20171029235511p:plain

  3. IntelliJ IDEA を再起動した後、プロジェクトで使用する Java SE を 8u152 へ変更します。

  4. 開いているプロジェクトを閉じて「Welcome to IntelliJ IDEA」ダイアログを表示します。

  5. ダイアログ下部の「Configure」-「Project Defaults」-「Project Structure」を選択します。

    f:id:ksby:20171029235702p:plain

  6. 「Default Project Structure」ダイアログが表示されます。画面左側で「Project Settings」-「Project」を選択後、画面右側の「Project SDK」の「New...」ボタンをクリックし、表示されるメニューから「JDK」を選択します。

    f:id:ksby:20171029235937p:plain

  7. 「Select Home Directory for JDK」ダイアログが表示されます。C:\Java\jdk1.8.0_152 を選択した後、「OK」ボタンをクリックします。

    f:id:ksby:20171030000120p:plain

  8. 「Default Project Structure」ダイアログに戻るので、今度は「Project SDK」の「Edit」ボタンをクリックします。

    f:id:ksby:20171030000304p:plain

  9. 画面左側で「Platform Settings」-「SDKs」が選択された状態になるので、画面右上の入力フィールドで "1.8" → "1.8.0_152" へ変更します。

    f:id:ksby:20171030000507p:plain

  10. 次に中央のリストから「1.8.0_144」を選択した後、リストの上の「-」ボタンをクリックして削除します。

    f:id:ksby:20171030000806p:plain

  11. 「OK」ボタンをクリックして「Default Project Structure」ダイアログを閉じます。

  12. 「Welcome to IntelliJ IDEA」ダイアログに戻ったら、ksbysample-webapp-lending プロジェクトを開きます。

  13. IntelliJ IDEA のメイン画面が開いたら、メニューから「File」-「Project Structure...」を選択します。

  14. 「Project Structure」ダイアログが表示されます。以下の画像の状態になっているので、

    f:id:ksby:20171030001028p:plain

    「Project SDK」と「Project language level」を選択し直します。

    f:id:ksby:20171030001240p:plain

  15. 「OK」ボタンをクリックして「Project Structure」ダイアログを閉じます。

  16. メイン画面に戻ると画面右下に「Indexing...」の表示が出るので、終了するまで待ちます。

    f:id:ksby:20171030001415p:plain

  17. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20171030002030p:plain

  18. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20171030002532p:plain

  19. 特に問題は発生しませんでした。8u152 で開発を進めます。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その31 )( テスト対象のブラウザに Headless Chrome と HtmlUnit を追加する+Chrome, Firefox, HtmlUnit で連続テストする gradle タスクを作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その30 )( Geb を 2.0 へバージョンアップする+Firefox headless モードを使用する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • テスト対象のブラウザに Headless ChromeHtmlUnit を追加します。
    • ChromeFirefox (どちらも Headless モード)と HtmlUnit で連続してテストする gradle のタスクを作成します。

参照したサイト・書籍

  1. ヘッドレス Chrome ことはじめ
    https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja

  2. Headless Chrome and Selenium on Windows?
    https://stackoverflow.com/questions/43880619/headless-chrome-and-selenium-on-windows

  3. SeleniumHQ/htmlunit-driver
    https://github.com/SeleniumHQ/htmlunit-driver

  4. Where to find 64 bit version of chromedriver.exe for Selenium WebDriver?
    https://stackoverflow.com/questions/23081507/where-to-find-64-bit-version-of-chromedriver-exe-for-selenium-webdriver

  5. 【入門】Geb+SpockではじめるWebテストクロスブラウザテスト編~ / Setting up and running of the cross-browser test
    http://yfj2.hateblo.jp/entry/2014/11/09/004011

  6. ChromeDriver - WebDriver for Chrome
    https://sites.google.com/a/chromium.org/chromedriver/

  7. geb/geb-example-gradle/build.gradle
    https://github.com/geb/geb-example-gradle/blob/master/build.gradle

目次

  1. Chrome と HtmlUnit 用の Selenium WebDriver をインストールする
  2. ChromeDriver をダウンロードして配置する
  3. GebConfig.groovy を修正する
  4. build.gradle にタスクを定義する
  5. 動作確認

手順

ChromeHtmlUnit 用の Selenium WebDriver をインストールする

build.gradle を以下のように変更します。

dependencies {
    ..........

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0")
    testCompile("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:htmlunit-driver:2.27")
    testCompile("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}
  • 以下の行を追加します。
    • testCompile("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    • testCompile("org.seleniumhq.selenium:htmlunit-driver:2.27")
  • HtmlUnit 用の WebDriver として selenium-htmlunit-driver がありますが、https://mvnrepository.com/selenium-htmlunit-driver を見てみると最新バージョンが 2016/2 にリリースされた 2.52.0 で、3.x 系がリリースされていませんでした。さすがに 3.6.0 と組み合わせるのは無理だろうと思い Web で探してみると、WebDriver compatible を謳っている htmlunit-driver が見つかり、こちらは 2017/6 に 2.27 がリリースされていましたので、htmlunit-driver をインストールすることにします。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

ChromeDriver をダウンロードして配置する

Chrome を操作するためには Firefox の geckodriver と同じように ChromeDriver が必要です。ダウンロードして配置します。

ChromeDrive の Downloads ページ の Latest Release の右側に表示されている「ChromeDriver 2.33」リンクをクリックして次のページへ進んだ後、「chromedriver_win32.zip」リンクをクリックして chromedriver_win32.zip をダウンロードします。64bit版は提供されていませんが、chromedriver_win32.zip のファイルで 64bit の Windows + Chrome でも問題なく動作します。

ダウンロード後、C:\chromedriver\2.33 ディレクトリを作成した後、chromedriver_win32.zip を解凍して出来る chromedriver.exe をその下に配置します。

f:id:ksby:20171029004812p:plain

GebConfig.groovy を修正する

src/test/resources/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions
import org.openqa.selenium.htmlunit.HtmlUnitDriver

System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.bat")
System.setProperty("webdriver.chrome.driver", "C:/chromedriver/2.33/chromedriver.exe")
driver = {
    FirefoxOptions firefoxOptions = new FirefoxOptions()
    firefoxOptions.setHeadless(true)
    new FirefoxDriver(firefoxOptions)
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}

environments {

    chrome {
        driver = {
            ChromeOptions chromeOptions = new ChromeOptions()
            chromeOptions.setHeadless(true)
            new ChromeDriver(chromeOptions)
        }
    }

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

    htmlunit {
        new HtmlUnitDriver(true)
    }

}
  • System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.bat")driver の中に書いていたのを外に出します。
  • System.setProperty("webdriver.chrome.driver", "C:/chromedriver/2.33/chromedriver.exe") を追加します。
  • environments { ... } の定義を追加し、この中に chrome, firefox, htmlunit の driver の定義を記述します。

build.gradle にタスクを定義する

build.gradle に各ブラウザ毎のテスト用タスクと、全てのブラウザのテスト用タスクを連続実行するためのタスクを追加します。このタスクは geb/geb-example-gradle/build.gradle からコピーしました。

..........

test {
    // test タスクの jvmArgs は tasks.withType(Test) { ... } で定義している
    exclude "geb/**"
}

// for Geb + Spock Integration Test
def drivers = ["chrome", "firefox", "htmlunit"]
drivers.each { driver ->
    task "${driver}Test"(type: Test) {
        // 前回実行時以降に何も更新されていなくても必ず実行する
        outputs.upToDateWhen { false }
        systemProperty "geb.env", driver
        exclude "ksbysample/**"
    }
}
task gebTest {
    dependsOn drivers.collect { tasks["${it}Test"] }
    enabled = false
}

tasks.withType(Test) {
    jvmArgs = ['-Dspring.profiles.active=unittest']
}

// for Doma-Gen
task domaGen {
    ..........
  • test タスクから jvmArgs = ['-Dspring.profiles.active=unittest'] を削除します。
  • 記述していた task gebTest(type: Test) { ... } を削除します。
  • // for Geb + Spock Integration Test から下の部分を記述します。リストからタスクを動的に作成したり、動的に作成されたタスクを依存関係に指定する方法があるとは、初めて知りました。。。
  • tasks.withType(Test) { ... } を追加し、Test タイプのタスク共通の設定はこちらに記述します。今回は jvmArgs のみ記述します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

更新すると Gradle projects の other の下に chromeTest, firefoxTest, gebTest, htmlunitTest の4つのタスクが表示されます。

f:id:ksby:20171029085625p:plain

動作確認

最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します。追加した chromeTest, firefoxTest, gebTest, htmlunitTest のタスクが実行されていないことも確認できます。

f:id:ksby:20171029090212p:plain

chromeTest タスクを実行します。ブラウザの画面が表示されず、テストは成功します。また ChromeDriver のメッセージが出力されており、Chrome がテストに使われていることが分かります。

f:id:ksby:20171029090534p:plain

firefoxTest タスクを実行します。こちらもブラウザの画面は表示されず、テストは成功します。

f:id:ksby:20171029090804p:plain

htmlunitTest タスクを実行します。HtmlUnit はブラウザではないので当然画面は表示されず、テストは成功します。

f:id:ksby:20171029091445p:plain

最後に gebTest タスクを実行します。chromeTest, firefoxTest, htmlunitTest の3つのタスクが実行されてそれぞれ成功し、gebTest タスクは build.gradle 内で enabled = false を記述しているので SKIP されることが確認できます。

f:id:ksby:20171029091741p:plain

ちなみに src/test/groovy/geb/gebspec/SimpleTestSpec.groovy をテストが失敗するように変更してから、

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        ..........

        then: "「お名前(漢字)」の必須チェックエラーのメッセージが表示される"
        $("#form-group-name .js-errmsg").displayed == true
//        $("#form-group-name .js-errmsg").text() == "お名前(漢字)を入力してください"
        $("#form-group-name .js-errmsg").text() == "エラーです"
    }

}

gebTest タスクを実行すると chromeTest タスクでエラーが出て先に進みません。

f:id:ksby:20171029092304p:plain

履歴

2017/10/29
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その30 )( Geb を 2.0 へバージョンアップする+Firefox headless モードを使用する )

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • Geb の 2.0 がリリースされたのでバージョンアップします。
    • geckodriver.exe の場所を jvm のオプション -Dwebdriver.gecko.driver= で指定するのではなく System.setProperty("webdriver.gecko.driver", "...") で指定するよう変更します。
    • Firefox に headless モードがあるようなので、headless モードを使用するよう設定を変更します。

参照したサイト・書籍

  1. Geb - Very Groovy Browser Automation
    http://www.gebish.org/

  2. Headless mode
    https://developer.mozilla.org/en-US/Firefox/Headless_mode

  3. Disable HttpClient logging
    https://stackoverflow.com/questions/4915414/disable-httpclient-logging

  4. How do I disable Firefox logging in Selenium using Geckodriver?
    https://stackoverflow.com/questions/41387794/how-do-i-disable-firefox-logging-in-selenium-using-geckodriver

目次

  1. Geb を 2.0-rc-1 → 2.0 へバージョンアップする
  2. geckodriver.exe の場所は System.setProperty("webdriver.gecko.driver", "...") で指定する
  3. Firefox headless モードを使用する
    1. GebConfig.groovy を変更する
    2. 動作確認
    3. 大量に出力される DEBUG ログ [Forwarding ... on session ... to remote] DEBUG org.apache.http を抑制する
    4. Marionette DEBUG のログを抑制する

手順

Geb を 2.0-rc-1 → 2.0 へバージョンアップする

build.gradle の以下の点を変更します。

dependencies {
    ..........
    def seleniumVersion = "3.6.0"

    ..........

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}
  • org.gebish:geb-spock のバージョン番号を 2.0-rc-12.0 に変更します。
  • def seleniumVersion = "3.6.0" を追加し、selenium のモジュールのバージョン番号を ${seleniumVersion} で指定するよう変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Tomcat を起動した後、test, gebTest タスクを実行して問題ないことを確認します(画面キャプチャは省略します)。この後もテストを繰り返すので Tomcat は起動したままにします。

geckodriver.exe の場所は System.setProperty("webdriver.gecko.driver", "...") で指定する

前回の記事では geckordriver.exe を build.gradle の gebTest タスクと、IntelliJ IDEA の「Run」-「Edit Configurations...」メニューで開いた先の JUnit の設定のところに記述しましたが、GebConfig.groovy に System.setProperty("webdriver.gecko.driver", "...") で定義すれば利用可能になることが分かったので、設定を変更します。

src/test/resources/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.firefox.FirefoxDriver

driver = {
    System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.exe")
    new FirefoxDriver()
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}

build.gradle の gebTest を以下のように変更します。

test {
    jvmArgs = ['-Dspring.profiles.active=unittest']
    exclude "geb/**"
}

task gebTest(type: Test) {
    jvmArgs = ['-Dspring.profiles.active=unittest']
    exclude "ksbysample/**"
}
  • jvmArgs の指定を '-Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe' を削除して test タスクと同じにします。

IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「JUnit」の「VM Options」から -Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe を削除します。

f:id:ksby:20171028080101p:plain

動作確認します。

最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します(画面キャプチャは省略します)。

次に gebTest タスクを実行すると Firefox が起動し、テストは成功します。

f:id:ksby:20171028080601p:plain

最後に src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を開き、左側のアイコンから Run '動作確認用()' を選択します。

f:id:ksby:20171028081146p:plain

こちらも Firefox が起動し、テストは成功します。

f:id:ksby:20171028082241p:plain

Firefox headless モードを使用する

Firefox に headless モードが実装されているらしいので、そちらを使うように設定してみます。

GebConfig.groovy を変更する

src/test/resources/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions

driver = {
    System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.exe")
    FirefoxOptions firefoxOptions = new FirefoxOptions()
    firefoxOptions.setHeadless(true)
    new FirefoxDriver(firefoxOptions)
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}
  • 以下の2行を追加します。
    • FirefoxOptions firefoxOptions = new FirefoxOptions()
    • firefoxOptions.setHeadless(true)
  • new FirefoxDriver() の引数に firefoxOptions を追加します。

動作確認

動作確認します。

最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します(画面キャプチャは省略します)。

次に gebTest タスクを実行してみると Firefox の画面は表示されず、テストは成功します。ただし DEBUG ログが大量に出ます。。。 これは出来れば出ないようにしたいですね。

f:id:ksby:20171028124025p:plain

DEBUG メッセージに対応する前に、うまく動いているのかちょっと分かりづらいので、テストの内容を少し変更してみます。

src/test/groovy/geb/page/inquiry/InquiryInput01Page.groovy を以下のように変更します。

package geb.page.inquiry

import geb.Page

class InquiryInput01Page extends Page {

    static url = "/inquiry/input/01"
    static at = { title == "入力フォーム - 入力画面1" }
    static content = {
        btnNext { $(".js-btn-next") }
    }

}
  • static content { ... } を追加し、その中に btnNext { $(".js-btn-next") } を記述します。

src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を以下のように変更し、最後でテストが失敗するようにします。

package geb.gebspec

import geb.page.inquiry.InquiryInput01Page
import geb.spock.GebSpec

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        given: "入力画面1へアクセスする"
        to InquiryInput01Page
        waitFor { at InquiryInput01Page }
        $("#form-group-name .js-errmsg").displayed == false

        when: "何も入力せずに「次へ」ボタンをクリックする"
        btnNext.click(InquiryInput01Page)

        then: "「お名前(漢字)」の必須チェックエラーのメッセージが表示される"
        $("#form-group-name .js-errmsg").displayed == true
//        $("#form-group-name .js-errmsg").text() == "お名前(漢字)を入力してください"
        $("#form-group-name .js-errmsg").text() == "エラーです"
    }

}

gebTest タスクを実行してみると今度はエラーになりました。Spock はエラーメッセージが分かりやすくていいですね。

f:id:ksby:20171028125357p:plain

src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を以下のように変更し、今度は成功するようにします。

package geb.gebspec

import geb.page.inquiry.InquiryInput01Page
import geb.spock.GebSpec

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        given: "入力画面1へアクセスする"
        to InquiryInput01Page
        waitFor { at InquiryInput01Page }
        $("#form-group-name .js-errmsg").displayed == false

        when: "何も入力せずに「次へ」ボタンをクリックする"
        btnNext.click(InquiryInput01Page)

        then: "「お名前(漢字)」の必須チェックエラーのメッセージが表示される"
        $("#form-group-name .js-errmsg").displayed == true
        $("#form-group-name .js-errmsg").text() == "お名前(漢字)を入力してください"
//        $("#form-group-name .js-errmsg").text() == "エラーです"
    }

}

gebTest タスクを実行してみると今度は成功します。headless モードでも問題なく動いているようです。

f:id:ksby:20171028130043p:plain

大量に出力される DEBUG ログ [Forwarding ... on session ... to remote] DEBUG org.apache.http を抑制する

何か情報がないか Google で検索すると、stackoverflow の以下の QA を見つけました。

これを読むと logback.xml があれば抑制できるようです。

src/test/resources の下に logback.xml を新規作成し、以下の内容を記述します。色々試してみたところ <logger .../> の定義がなくても logback.xml があれば DEBUG ログを抑制できるようです。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        Geb+Spock で Firefox を headless モードで動かした時に大量に以下の DEBUG ログ
        が出力されるのを抑制するために作成しているファイルである。ファイルがあればログを抑
        制できるので、特に logger の定義は記述していない。

        [Forwarding ... on session ... to remote] DEBUG org.apache.http
     -->
</configuration>

gebTest タスクを実行してみると [Forwarding ... on session ... to remote] DEBUG org.apache.http の DEBUG メッセージは出力されました。ただし、まだ Marionette DEBUG というログが出力されています。

f:id:ksby:20171028133928p:plain

Marionette DEBUG のログを抑制する

Google で検索していろいろ見たのですが、stackoverflow の以下の QA を見て解決しました。

geckodriver.exe を配置した C:\geckodriver\0.19.0 の下に geckodriver.bat を新規作成し、以下の内容を記述します。

@echo off
C:\geckodriver\0.19.0\geckodriver.exe --log info %* > NUL 2>&1

src/test/groovy/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions

driver = {
    System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.bat")
    FirefoxOptions firefoxOptions = new FirefoxOptions()
    firefoxOptions.setHeadless(true)
    new FirefoxDriver(firefoxOptions)
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}
  • System.setProperty("webdriver.gecko.driver", "...") の第2引数に設定していたプログラム名を C:/geckodriver/0.19.0/geckodriver.exeC:/geckodriver/0.19.0/geckodriver.bat に変更します。

gebTest タスクを実行すると Marionette DEBUG のログは全く出力されなくなりました。

f:id:ksby:20171028143338p:plain

履歴

2017/10/28
初版発行。