LoginSignup
5
9

More than 1 year has passed since last update.

Selenideの操作処理(Kotlin)を自動生成してみる。

Last updated at Posted at 2021-05-15

はじめに

Kotlin + Selenide実践入門で記載させていただいたように、KotlinとSelenideを利用すれば、少ない行数でテストが実装可能ですが、課題として、HTMLの要素を一意に特定するCssSelectorなどの識別子の収集が残ります。本投稿では、HTMLの要素を一意に特定するCssSelectorを一意識別子と記載します。

複数ページを対象とし、ページ遷移を行いながらテスト操作を生成するのでれば、機械学習的な要素が不可欠であると思いますが、単一ページで、対象要素(htmlのタグ)をしぼれば難易度は低いと言えます。まあ、テストを手で実装する際に一意識別子を調べてテストコードに落とすときの手間が省けるようにとの意図のテスト操作との前提ですが。

本投稿では、Selenideの操作処理(Kotlin)の自動生成処理を実装します。
対象は以下の要素となります。

  • input button
  • input text
  • input radio
  • input checkbox
  • select

完成版の全ソースは
GitHubリポジトリ
に登録しております。

利用するライブラリ

  • Selenide
  • jsoup HTMLを解析するために利用します。
  • Apache FreeMarker 操作処理を含んだクラスのテンプレートを管理するために利用します。
  • JUnit5
  • MockK
  • Apache Commons Text

課題の確認

課題としては、以下の2点となります。

  • 一意識別子を収集できるツールが存在しない。できるとしても手間がかかる。
  • 一意識別子を指定したからといって、対象要素が操作可能とは限らない。

一意識別子を収集できるツールが存在しない。できるとしても手間がかかる。

「Chrome Developer Tools」や「Selenium IDE」を利用すればある程度の楽ができますが、いろいろ足りないですし、手作業がどうしても必要となります。

例として、以下のHTMLで説明させていただきます。まあこんなの実際にはありえないレベルですが・・・

  <div id="p-input1-2-1">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>
  <div id="p-input1-2-2">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>

  <BR>
  <input type="text" name="input1-1" id="input1" maxlength="10" value="input1-1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-2" hoge="1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="2">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  <BR>

「Chrome Developer Tools」からcopy celectorを行った結果が以下の画像となります。画像サイズが無駄に大きくなるので、全ての#input1の結果は取得していないのですが、全てのCssSelectorが#input1となってしまいます。
セレクタ同じになる.gif

「Selenium IDE」は「Chrome Developer Tools」に比べると賢いです。結果は以下のようになります。
seleniumide.png
全部正しいですね・・・、残念・・・
まあ「Selenium IDE」でJUnit形式のソースを生成しても、コードはWebDriverの直操作ですので動的な要素の変化に弱い点が課題だと感じています。

課題の解決策

  • 一意識別子を収集できるツールが存在しない。できるとしても手間がかかる。
    Selenideの操作処理(Kotlin)を自動生成することで解決します。

  • 一意識別子を指定したからといって、対象要素が操作可能とは限らない。
    本投稿では、全てのタグ対応すると分かりにくくなりますので、ボタンだけ対応します。

操作処理の自動生成時に実際にボタンを押し、押せなればJavaScriptで押す処理を生成します。

生成処理の一部
        var usingJavaScript = false

        try {
            Selenide.`$`(By.cssSelector(cssSelector)).click()
        } catch (e: Throwable) {
            when (e) {
                //UIAssertionError:非表示の要素をクリック、InvalidStateException他の要素によりクリックがブロック
                is UIAssertionError -> {
                    testOperationList.add("// clickByCssSelector fail")
                    usingJavaScript = true
                }
                else -> throw e
            }
        }

usingJavaScripttrueの場合は以下の操作が生成されます。

    @Test
    fun `button3 test operation`() {
        /**************** cssSelector #notDisplayButton の処理 start ****************/
        // clickByCssSelector fail
        //Selenide.`$`(By.cssSelector("#notDisplayButton")).click()

        val driver = WebDriverRunner.getWebDriver()
        val executor = driver as JavascriptExecutor
        val element = Selenide.`$`(By.cssSelector("#notDisplayButton"))
        executor.executeScript("arguments[0].click()", element)    
    }

usingJavaScriptfalseの場合は以下の操作が生成されます。

    @Test
    fun `button3 test operation`() {
        /**************** cssSelector #notDisplayButton の処理 start ****************/
        // clickByCssSelector fail
        Selenide.`$`(By.cssSelector("#notDisplayButton")).click()        
    }

実際に生成する操作処理は、テスト容易性のためにSelenide.をユーティリティクラスのメソッドでラップする形になります。一意識別子での要素の存在確認処理も含まれます。

自動生成の動作イメージと実行方法

自動生成の動作イメージ

プロジェクトのパスはC:\workspace\SelenideOperationGeneratorの前提となります。

動作後に、C:\workspace\SelenideOperationGenerator\src\test\kotlin\outputにktファイルが作成されます。
image.gif

実行方法

TestExampleOperationGenerator#generateを呼び出して実行します。

Main.kt
package jp.small_java_world.testopegen

import com.codeborne.selenide.Selenide
import jp.small_java_world.testopegen.define.CommonDef.Companion.PROJECT_ROOT_PATH
import jp.small_java_world.testopegen.util.SelenideUtil

fun main(args: Array<String>) {
    val testExampleOperationGenerator= TestExampleOperationGenerator()
    val targetHtmlFullPath = "file://$PROJECT_ROOT_PATH/html/input.html"

    testExampleOperationGenerator.generate(
        "testOperationClassTemplate.ftl",
        "InputTest",
        listOf(
            "Selenide.open(\"$targetHtmlFullPath\")"
        )
    )
    {
        Selenide.open(targetHtmlFullPath)
    }
}

CommonDef.ktで
const val PROJECT_ROOT_PATH = "C:/workspace/SelenideOperationGenerator"
と宣言しております。

TestExampleOperationGenerator#generateの第1引数

FreeMarkerのテンプレートのファイル名となります。

testOperationClassTemplate.ftlは以下のようになります。

package output

import com.codeborne.selenide.Configuration
import com.codeborne.selenide.Selenide
import com.codeborne.selenide.WebDriverRunner
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import jp.small_java_world.testopegen.util.*

class ${testClassName} {
    companion object {
        @JvmStatic
        @BeforeAll
        fun beforeAll() {
            Configuration.browser = WebDriverRunner.CHROME;
        }
    }

    @BeforeEach
    fun beforeEach() {
        ${previousAction}
    }

    <#list testMethodDataList as test>
    @Test
    fun `${test.name}`() {
        ${test.operation}
    }

    </#list>
}

TestExampleOperationGenerator#generateの第2引数

生成するクラスのファイル名となります。

TestExampleOperationGenerator#generateの第3引数

生成するクラスの前処理となるList<String>です。

        listOf(
            "Selenide.open(\"$targetHtmlFullPath\")"
        )

と指定した場合、生成したクラスのBeforeEachは以下のようになります。
targetHtmlFullPathが展開されているのでわかりにくいですが・・・

    @BeforeEach
    fun beforeEach() {
        Selenide.open("file://C:/workspace/SelenideOperationGenerator/html/input.html")
    }

TestExampleOperationGenerator#generateの第4引数

操作処理の生成を行うための前処理の無名関数となります。第3引数と同じ内容を意味する処理を指定してください。

実装

必要な構成要素

必要な構成要素は以下のようになります。

  • Selenideの操作をラップするユーティリティクラス
  • HTMLの要素の種別(input textなど)を識別できるenumの作成
  • 対象ページからorg.jsoup.select.Elementsを抽出する処理
  • org.jsoup.select.Elementから一意識別子とTargetElementTypeを取得する処理
  • 一意識別子とTargetElementTypeから操作処理を生成する処理
  • 結果をテンプレートに埋め込む処理
  • 結果をファイルに出力する処理

Selenideの操作をラップするユーティリティクラス

生成する操作処理を簡単にする目的と、テスト容易性のため、以下のようなユーティリティクラスを作成します。

package jp.small_java_world.testopegen.util

import com.codeborne.selenide.Condition.*
import com.codeborne.selenide.ElementsCollection
import com.codeborne.selenide.Selenide
import com.codeborne.selenide.SelenideElement
import com.codeborne.selenide.WebDriverRunner
import org.openqa.selenium.By
import org.openqa.selenium.JavascriptExecutor

class SelenideUtil {
    companion object {
        @JvmStatic
        fun selectByName(tagName: String): SelenideElement {
            return Selenide.`$`(By.name(tagName))
        }

        @JvmStatic
        fun selectByClassName(className: String): SelenideElement {
            return Selenide.`$`(By.className(className))
        }

        @JvmStatic
        fun selectByCssSelector(cssSelector: String): SelenideElement {
            return Selenide.`$`(By.cssSelector(cssSelector))
        }

        @JvmStatic
        fun selectListByCssSelector(cssSelector: String): ElementsCollection {
            return Selenide.`$$`(By.cssSelector(cssSelector))
        }

        @JvmStatic
        fun selectById(id: String): SelenideElement {
            return Selenide.`$`(By.id(id))
        }

        @JvmStatic
        fun clickByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).click()
        }

        @JvmStatic
        fun clickByCssSelectorUseJS(cssSelector: String) {
            val driver = WebDriverRunner.getWebDriver()
            val executor = driver as JavascriptExecutor
            val element = selectByCssSelector(cssSelector)
            executor.executeScript("arguments[0].click()", element)
        }

        @JvmStatic
        fun inputTextByCssSelector(cssSelector: String, text: String) {
            selectByCssSelector(cssSelector).value = text
        }

        @JvmStatic
        fun shouldBeValueByCssSelector(cssSelector: String, expect: String) {
            selectByCssSelector(cssSelector).shouldBe(value(expect))
        }

        @JvmStatic
        fun selectRadioByCssSelector(cssSelector: String, value: String) {
            selectByCssSelector(cssSelector).selectRadio(value)
        }

        @JvmStatic
        fun selectOptionByValueByCssSelector(cssSelector: String, value: String) {
            selectByCssSelector(cssSelector).selectOptionByValue(value)
        }

        @JvmStatic
        fun selectOptionByCssSelector(cssSelector: String, value: String) {
            selectByCssSelector(cssSelector).selectOption(value)
        }

        @JvmStatic
        fun checkByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).isSelected = true
        }

        @JvmStatic
        fun unCheckByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).isSelected = false
        }

        @JvmStatic
        fun shouldBeSelectedByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).shouldBe(selected)
        }

        @JvmStatic
        fun shouldBeNotSelectedByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).shouldNotBe(selected)
        }

        @JvmStatic
        fun shouldHaveAttributeByCssSelector(cssSelector: String, attrName: String, attrValue: String) {
            selectByCssSelector(cssSelector).shouldHave(attribute(attrName, attrValue))
        }

        @JvmStatic
        fun confirmExistenceByCssSelector(cssSelector: String): Boolean {
            return try {
                Selenide.`$$`(By.cssSelector(cssSelector)).shouldHaveSize(1)
                true;
            } catch (e: Throwable) {
                false;
            }
        }

        @JvmStatic
        fun isDuplicateByCssSelector(cssSelector: String): Boolean {
            return Selenide.`$$`(By.cssSelector(cssSelector)).size !in (listOf(0, 1))
        }

        @JvmStatic
        fun getValueByCssSelector(cssSelector: String): String? {
            return selectByCssSelector(cssSelector).value
        }

        @JvmStatic
        fun getNameByCssSelector(cssSelector: String): String? {
            return selectByCssSelector(cssSelector).getAttribute("name")
        }
    }
}

HTMLの要素の種別(input textなど)を識別できるenum

enum TargetElementTypeを作成します。

package jp.small_java_world.testopegen.define

const val TAG_NAME_INPUT = "input"
const val TAG_NAME_SELECT = "select"

enum class TargetElementType(val tagName: String, val type: String, val tagNameJp: String) {
    INPUT_TEXT(TAG_NAME_INPUT, "text", "テキストボックス"),
    INPUT_BUTTON(TAG_NAME_INPUT, "button", "ボタン"),
    INPUT_RADIO(TAG_NAME_INPUT, "radio", "ラジオボタン"),
    INPUT_CHECKBOX(TAG_NAME_INPUT, "checkbox", "チェックボックス"),
    SELECT(TAG_NAME_SELECT, "select", "セレクトボックス")
}

対象ページからorg.jsoup.select.Elementsを抽出する処理

package jp.small_java_world.testopegen

import com.codeborne.selenide.WebDriverRunner
import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import org.jsoup.select.Elements

class HtmlDocumentParser {
    companion object {
        @JvmStatic
        fun getElements(previousAction: () -> Unit, targetTagList: List<String> = listOf("input", "select")): Elements {
            // { Selenide.open("file:///C:/example/input.html") }のような、
            // Selenideで生成対象ページを開く前処理の無名関数の呼び出し 
            previousAction.invoke()

            //Selenideで生成対象ページのhtmlを取得
            val driver = WebDriverRunner.getWebDriver()
            val html = driver.pageSource

            //Jsoupでhtmlを解析
            val htmlDocument = Jsoup.parse(html, "", Parser.htmlParser())

            var result = Elements()

            for (targetTag in targetTagList) {
                //タグ(targetTag)のorg.jsoup.select.Elementsを取得しresultに追加
                result.addAll(htmlDocument.getElementsByTag(targetTag))
            }

            return result
        }
    }
}

fun getElements(previousAction: () -> Unit, targetTagList: List<String>): Elements {
のpreviousActionは、TestExampleOperationGenerator#generateの第4引数で説明させていただいた無名関数となります。

targetTagList: List<String>は、listOf("input", "select")を指定する想定となりますので、デフォルト引数を指定しています。

            //Selenideで生成対象ページのhtmlを取得
            val driver = WebDriverRunner.getWebDriver()
            val html = driver.pageSource

            //Jsoupでhtmlを解析
            val htmlDocument = Jsoup.parse(html, "", Parser.htmlParser())

でhtmlを取得、Jsoupでhtmlを解析しています。

            for (targetTag in targetTagList) {
                //タグ(targetTag)のorg.jsoup.select.Elementsを取得しresultに追加
                result.addAll(htmlDocument.getElementsByTag(targetTag))
            }

org.jsoup.select.Elements resultに各タグに対応する結果を追加しています。

org.jsoup.select.Elementから一意識別子とTargetElementTypeを取得する処理

一意識別子の導出ロジック

コードの説明の前に導出ロジックを説明いたします。

以前のhtmlと同じですが、以下のhtmlの#p-input1-2-1 > #input1を例に説明させていただきます。

  <div id="p-input1-2-1">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>
  <div id="p-input1-2-2">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>

  <BR>
  <input type="text" name="input1-1" id="input1" maxlength="10" value="input1-1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-2" hoge="1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="2">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  <BR>

一意識別子になりえる候補のCssSelectorを作成します。
CssSelectorと判定優先順位は以下のようになります。

  1. #input1
  2. input[name='input1-2']
  3. input[hoge='3']
  4. #p-input1-2-1 > #input1
  5. #p-input1-2-1 > input[name='input1-2']
  6. #p-input1-2-1 > input[hoge='3']

上位からSelenideUtil#isDuplicateByCssSelectorで一意判定を行い、一意の場合は一意識別子として認定します。
結果として、#p-input1-2-1 > #input1が一意識別子となります。

SelenideUtil
        @JvmStatic
        fun isDuplicateByCssSelector(cssSelector: String): Boolean {
            return Selenide.`$$`(By.cssSelector(cssSelector)).size !in (listOf(0, 1))
        }

CssSelectorAnalyzer#getCssSelectorElementTypePair

実装する処理は、CssSelectorAnalyzer#getCssSelectorElementTypePairとなります。
呼び出し方法のイメージから入った方が理解しやすいと思います。

        //inputタグとselectタグのorg.jsoup.select.Elementsを取得
        val targetElements = HtmlDocumentParser.getElements(previousAction)

        //各タグを処理していく
        for (targetElement in targetElements) {
            // 現在の処理対象のinputTagElementに対応する Pair<String?, TargetElementType?>を取得
            var selectorElementTypePair = cssSelectorAnalyzer.getCssSelectorElementTypePair(targetElement)
       }

CssSelectorAnalyzer

CssSelectorAnalyzerは以下のようになります。

package jp.small_java_world.testopegen.analyzer

import jp.small_java_world.testopegen.define.TAG_NAME_INPUT
import jp.small_java_world.testopegen.define.TAG_NAME_SELECT
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil
import org.jsoup.nodes.Attribute
import org.jsoup.nodes.Element

class CssSelectorAnalyzer {
    /**
     * targetElementを一意に識別可能なCssSelectorの文字列がfirst
     * targetElementに対応するTargetElementTypeがsecondの
     * Pair<String?, TargetElementType?>を返却します。
     *
     * targetElement.cssSelector()で要素が重複していない場合は、targetElement.cssSelector()の値を返却します。
     *
     * tagName[attrName='attrValue']のcssSelectorで一意になるか確認する。
     * タグを限定すればid or nameで一意な可能性は高いのでこの属性を優先処理
     * inputであれば、input[id='idValue']とinput[name='nameValue']など
     * targetElement単独の評価で一意にならない場合は、
     * 親要素のCssSelector > input[id='idValue']などでの評価を行う。
     *
     * @param targetElement org.jsoup.nodes.Node
     * @return Pair<String?, TargetElementType?>
     */
    fun getCssSelectorElementTypePair(targetElement: Element?): Pair<String?, TargetElementType?> {
        checkNotNull(targetElement) { "targetElement cannot be null" }

        val tagName = targetElement.tagName()
        val inputType = targetElement.attr("type")
        val elementType = getElementType(tagName, inputType)

        var cssSelectorValue = targetElement.cssSelector()
        //targetElement.cssSelector()で要素が重複していない場合は、targetElement.cssSelector()の値を返却
        if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue!!)) {
            return cssSelectorValue to elementType
        }

        // 親要素のCssSelectorを取得
        val parentCssSelectorValue = getParentCssSelector(targetElement)

        // 評価する属性のリストの作成のために全属性のリストを取得
        val attributes = targetElement.attributes()
        val idAttribute = Attribute("id", attributes.get("id"))
        val nameAttribute = Attribute("name", attributes.get("name"))

        //idとnameとタグごとの除外属性を除去
        (getRemoveAttrNameList(elementType) + listOf("id", "name")).forEach { attributes.remove(it) }

        //idとname属性を優先するattributeListを生成
        val attributeList = listOf(idAttribute, nameAttribute) + attributes

        // まずは、対象のtagName[attrName='attrValue']で判定
        // うまくいかない場合、親のCssSelector > 対象のtagName[attrName='attrValue']で判定
        for (parentCssSelector in listOf(null, parentCssSelectorValue)) {
            for (attribute in attributeList) {
                //targetElement単独でのtagName[attrName='attrValue']のcssSelectorの文字列生成 idの場合は#id
                val targetCssSelector =
                    attribute.run { if (key.equals("id")) "#${value}" else "$tagName[${key}='${value}']" }

                //parentCssSelectorを加味したのcssSelectorの文字列生成
                cssSelectorValue =
                    parentCssSelector?.let {
                        "$it > $targetCssSelector"
                    } ?: targetCssSelector

                //cssSelectorValueでHTML要素が一意であれば結果をリターン
                if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue)) {
                    return cssSelectorValue to elementType
                }
            }
        }

        return null to null
    }

    private fun getRemoveAttrNameList(targetElementType: TargetElementType?): List<String> {
        return when (targetElementType) {
            TargetElementType.INPUT_TEXT -> listOf("type", "value", "size", "maxlength")
            else -> listOf("type")
        }
    }

    private fun getParentCssSelector(targetElement: Element?): String? {
        val parent = targetElement?.parent() ?: return null
        var cssSelectorValue = parent.cssSelector()
        if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue)) {
            return cssSelectorValue
        }

        cssSelectorValue = "${getParentCssSelector(parent)} > $parent.tagName()"
        if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue)) {
            return cssSelectorValue
        }

        return null
    }

    private fun getElementType(tagName: String?, inputType: String?): TargetElementType? {
        return when (tagName) {
            TAG_NAME_INPUT -> {
                when (inputType) {
                    TargetElementType.INPUT_BUTTON.type -> TargetElementType.INPUT_BUTTON
                    TargetElementType.INPUT_TEXT.type -> TargetElementType.INPUT_TEXT
                    TargetElementType.INPUT_RADIO.type -> TargetElementType.INPUT_RADIO
                    TargetElementType.INPUT_CHECKBOX.type -> TargetElementType.INPUT_CHECKBOX
                    else -> null
                }
            }
            TAG_NAME_SELECT -> TargetElementType.SELECT
            else -> null
        }
    }
}

tagNameinputTypeからelementType:TargetElementTypeを決定しています。
getElementTypeは、面白くないですね・・・

CssSelectorAnalyzer
        val tagName = targetElement.tagName()
        val inputType = targetElement.attr("type")
        val elementType = getElementType(tagName, inputType)
CssSelectorAnalyzer
    private fun getElementType(tagName: String?, inputType: String?): TargetElementType? {
        return when (tagName) {
            TAG_NAME_INPUT -> {
                when (inputType) {
                    TargetElementType.INPUT_BUTTON.type -> TargetElementType.INPUT_BUTTON
                    TargetElementType.INPUT_TEXT.type -> TargetElementType.INPUT_TEXT
                    TargetElementType.INPUT_RADIO.type -> TargetElementType.INPUT_RADIO
                    TargetElementType.INPUT_CHECKBOX.type -> TargetElementType.INPUT_CHECKBOX
                    else -> null
                }
            }
            TAG_NAME_SELECT -> TargetElementType.SELECT
            else -> null
        }
    }

ここから一意識別子の判定となります。

CssSelectorAnalyzer
        var cssSelectorValue = targetElement.cssSelector()
        //targetElement.cssSelector()で要素が重複していない場合は、targetElement.cssSelector()の値を返却
        if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue!!)) {
            return cssSelectorValue to elementType
        }

org.jsoup.nodes.Element#cssSelectorの結果が一意識別子であるか判定しています。

CssSelectorAnalyzer
        // 親要素のCssSelectorを取得
        val parentCssSelectorValue = getParentCssSelector(targetElement)

親要素のCssSelectorを取得しています。
getParentCssSelectorですが、要素のorg.jsoup.nodes.Element#cssSelectorの結果が一意識別子であれば、その値を適用、そうでなればもう一段上がって、との処理となります。

CssSelectorAnalyzer
    private fun getParentCssSelector(targetElement: Element?): String? {
        val parent = targetElement?.parent() ?: return null
        var cssSelectorValue = parent.cssSelector()
        if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue)) {
            return cssSelectorValue
        }

        cssSelectorValue = "${getParentCssSelector(parent)} > $parent.tagName()"
        if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue)) {
            return cssSelectorValue
        }

        return null
    }

ここから本番です。

CssSelectorAnalyzer
        // 評価する属性のリストの作成のために全属性のリストを取得
        val attributes = targetElement.attributes()
        val idAttribute = Attribute("id", attributes.get("id"))
        val nameAttribute = Attribute("name", attributes.get("name"))

        //idとnameとタグごとの除外属性を除去
        (getRemoveAttrNameList(elementType) + listOf("id", "name")).forEach { attributes.remove(it) }

CssSelectorの要素になる属性をリストアップしています。
org.jsoup.nodes.Element#attributesで属性の一覧を取得し、idnameの属性を取り出したのちに、attributesから削除します。
idnameは優先して判定に利用したいので、後で先頭に持ってきます。

CssSelectorAnalyzer
        //idとname属性を優先するattributeListを生成
        val attributeList = listOf(idAttribute, nameAttribute) + attributes

idnameは優先して判定に利用したいので、新たなリストを生成します。

準備は整ったので、あとはループして判定するだけです。

CssSelectorAnalyzer
        // まずは、対象のtagName[attrName='attrValue']で判定
        // うまくいかない場合、親のCssSelector > 対象のtagName[attrName='attrValue']で判定
        for (parentCssSelector in listOf(null, parentCssSelectorValue)) {
            for (attribute in attributeList) {
                //targetElement単独でのtagName[attrName='attrValue']のcssSelectorの文字列生成 idの場合は#id
                val targetCssSelector =
                    attribute.run { if (key.equals("id")) "#${value}" else "$tagName[${key}='${value}']" }

                //parentCssSelectorを加味したのcssSelectorの文字列生成
                cssSelectorValue =
                    parentCssSelector?.let {
                        "$it > $targetCssSelector"
                    } ?: targetCssSelector

                //cssSelectorValueでHTML要素が一意であれば結果をリターン
                if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue)) {
                    return cssSelectorValue to elementType
                }
            }
        }

CssSelectorAnalyzerのテスト

CssSelectorAnalyzerTestは以下のようになります。

実際にブラウザを動かして、各Htmlの要素のみ含んだHTMLを解析して結果を検証するようになります。

CssSelectorAnalyzerTest
package jp.small_java_world.testopegen.analyzer

import com.codeborne.selenide.Configuration
import com.codeborne.selenide.Selenide
import com.codeborne.selenide.WebDriverRunner
import jp.small_java_world.testopegen.define.CommonDef.Companion.PROJECT_ROOT_PATH
import jp.small_java_world.testopegen.define.TargetElementType
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.parser.Parser
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test

class CssSelectorAnalyzerTest {
    companion object {
        @JvmStatic
        @BeforeAll
        fun beforeAll() {
            Configuration.browser = WebDriverRunner.CHROME;
        }

        @JvmStatic
        @AfterAll
        fun afterAll() {
            WebDriverRunner.closeWebDriver()
        }
    }

    @Test
    fun `test getCssSelectorElementTypePair input button`() {
        val expectedResultList =
            listOf(
                "#addButton" to TargetElementType.INPUT_BUTTON,
                "#deleteButton" to TargetElementType.INPUT_BUTTON,
                "#delayDeleteButton" to TargetElementType.INPUT_BUTTON,
                "#notDisplayButton" to TargetElementType.INPUT_BUTTON
            )

        testGetCssSelectorElementTypePairCommon("input_button.html", expectedResultList)
    }

    private fun testGetCssSelectorElementTypePairCommon(
        htmlFileName: String,
        expectedResultList: List<Pair<String, TargetElementType>>
    ) {
        val inputTagElements = openAndGetElements(htmlFileName)
        assertEquals(expectedResultList.size, inputTagElements.size)

        val cssSelectorAnalyzer = CssSelectorAnalyzer()
        for ((index, inputTagElement) in inputTagElements.withIndex()) {
            assertEquals(expectedResultList[index], cssSelectorAnalyzer.getCssSelectorElementTypePair(inputTagElement))
        }
    }

    private fun openAndGetElements(htmlFileName: String): List<Element> {
        Selenide.open("file:///$PROJECT_ROOT_PATH/html/$htmlFileName")
        val driver = WebDriverRunner.getWebDriver()
        val html = driver.pageSource
        val doc = Jsoup.parse(html, "", Parser.htmlParser())
        return doc.getElementsByTag("input") + doc.getElementsByTag("select")
    }

    @Test
    fun `test getCssSelectorElementTypePair input radio`() {
        val expectedResultList =
            listOf(
                "#grade1" to TargetElementType.INPUT_RADIO,
                "#grade2" to TargetElementType.INPUT_RADIO
            )

        testGetCssSelectorElementTypePairCommon("input_radio.html", expectedResultList)
    }

    @Test
    fun `test getCssSelectorElementTypePair input checkbox`() {
        val expectedResultList =
            listOf(
                "#lang1" to TargetElementType.INPUT_CHECKBOX,
                "#lang2" to TargetElementType.INPUT_CHECKBOX,
            )

        testGetCssSelectorElementTypePairCommon("input_checkbox.html", expectedResultList)
    }

    @Test
    fun `test getCssSelectorElementTypePair input text`() {
        val expectedResultList =
            listOf(
                "#p-input1-2-1 > #input1" to TargetElementType.INPUT_TEXT,
                "#p-input1-2-2 > #input1" to TargetElementType.INPUT_TEXT,
                "input[name='input1-1']" to TargetElementType.INPUT_TEXT,
                "input[hoge='1']" to TargetElementType.INPUT_TEXT,
                "input[hoge='2']" to TargetElementType.INPUT_TEXT,
                "html > body > input[hoge='3']" to TargetElementType.INPUT_TEXT
            )

        testGetCssSelectorElementTypePairCommon("input_text.html", expectedResultList)
    }

    @Test
    fun `test getCssSelectorElementTypePair select`() {
        val expectedResultList =
            listOf(
                "html > body > select" to TargetElementType.SELECT
            )

        testGetCssSelectorElementTypePairCommon("select.html", expectedResultList)
    }
}

テスト対象の各HTMLとテストメソッドの結果のCssSelectorのリストを見ていただくと、テストのイメージが伝わりやすいと思います。

input button

CssSelectorAnalyzerTest
    @Test
    fun `test getCssSelectorElementTypePair input button`() {
        val expectedResultList =
            listOf(
                "#addButton" to TargetElementType.INPUT_BUTTON,
                "#deleteButton" to TargetElementType.INPUT_BUTTON,
                "#delayDeleteButton" to TargetElementType.INPUT_BUTTON,
                "#notDisplayButton" to TargetElementType.INPUT_BUTTON
            )

        testGetCssSelectorElementTypePairCommon("input_button.html", expectedResultList)
    }
input_button.htmlのbody
<body>
  <h1>操作対象 input button サンプル</h1>
  <label>button:</label>
  <input type="button" id="addButton" value="追加ボタン">
  <input type="button" id="deleteButton" value="削除ボタン">
  <input type="button" id="delayDeleteButton" value="遅延削除ボタン">
  <input type="button" id="notDisplayButton" name="notDisplayButton" value="非表示のボタン">
</body>

input radio

CssSelectorAnalyzerTest
    @Test
    fun `test getCssSelectorElementTypePair input radio`() {
        val expectedResultList =
            listOf(
                "#grade1" to TargetElementType.INPUT_RADIO,
                "#grade2" to TargetElementType.INPUT_RADIO
            )

        testGetCssSelectorElementTypePairCommon("input_radio.html", expectedResultList)
    }
input_radio.htmlのbody
<body>
  <h1>操作対象 input radio サンプル</h1>
  <label>radio:</label>
  <input type="radio" name="grade" id="grade1" value="1"><label for="grade1">1年生</label>
  <input type="radio" name="grade" id="grade2" value="2"><label for="grade2">2年生</label>
</body>

input checkbox

CssSelectorAnalyzerTest
    @Test
    fun `test getCssSelectorElementTypePair input checkbox`() {
        val expectedResultList =
            listOf(
                "#lang1" to TargetElementType.INPUT_CHECKBOX,
                "#lang2" to TargetElementType.INPUT_CHECKBOX,
            )

            testGetCssSelectorElementTypePairCommon("input_checkbox.html", expectedResultList)
    }
input_checkbox.htmlのbody
<body>
  <h1>操作対象 input checkbox サンプル</h1>
  <label>checkbox:</label>
  <input type="checkbox" name="lang" id="lang1" value="1"><label for="lang1">Java</label>
  <input type="checkbox" name="lang" id="lang2" value="2"><label for="lang2">C#</label>
</body>

input text

CssSelectorAnalyzerTest
    @Test
    fun `test getCssSelectorElementTypePair input text`() {
        val expectedResultList =
            listOf(
                "#p-input1-2-1 > #input1" to TargetElementType.INPUT_TEXT,
                "#p-input1-2-2 > #input1" to TargetElementType.INPUT_TEXT,
                "input[name='input1-1']" to TargetElementType.INPUT_TEXT,
                "input[hoge='1']" to TargetElementType.INPUT_TEXT,
                "input[hoge='2']" to TargetElementType.INPUT_TEXT,
                "html > body > input[hoge='3']" to TargetElementType.INPUT_TEXT
            )

        testGetCssSelectorElementTypePairCommon("input_text.html", expectedResultList)
    }
input_checkbox.htmlのbody
<body>
  <h1>操作対象 input text サンプル</h1>
  <label>text:</label>
  <div id="p-input1-2-1">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>
  <div id="p-input1-2-2">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>
  <BR>
  <input type="text" name="input1-1" id="input1" maxlength="10" value="input1-1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-2" hoge="1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="2">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
</body>

一意識別子とTargetElementTypeから操作処理を生成する処理

インターフェースとクラスの構成

OperationGenerator#generateCustomOperationを各実装クラスが実装することで処理を実現します。
class.png

OperationGeneratorは以下の通りです。

OperationGenerator
package jp.small_java_world.testopegen.generator

import jp.small_java_world.testopegen.define.CommonDef
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil

interface OperationGenerator {
    fun generateCustomOperation(cssSelector: String?, testOperationList: MutableList<String>): Collection<String>
    fun getElementType(): TargetElementType

    fun generateOperation(cssSelector: String?): MutableList<String> {
        var testOperationList = mutableListOf<String>()
        if (addConfirmOperation(cssSelector, getElementType().tagNameJp, testOperationList)) {
            generateCustomOperation(cssSelector, testOperationList)
        }

        return testOperationList;
    }

    fun addConfirmOperation(
        cssSelector: String?,
        elementName: String,
        testOperationList: MutableList<String>
    ): Boolean {
        testOperationList.add("/**************** cssSelector ${cssSelector} の処理 start ****************/")

        if (!SelenideUtil.confirmExistenceByCssSelector(cssSelector!!)) {
            testOperationList.add("//confirmExistenceByCssSelector fail")
            return false
        }

        val confirmOperation =
            CommonDef.CONFIRM_EXISTENCE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
        testOperationList.add("//${elementName}の存在確認")
        testOperationList.add(confirmOperation)
        testOperationList.add("")

        return true
    }
}

実際の処理のエントリーポイントは、OperationGenerator.generateOperation(cssSelector: String?)で、cssSelectorに対応する操作処理を含んだCollectionが返却されます。

if (addConfirmOperation(cssSelector, getElementType().tagNameJp, testOperationList))
でcssSelectorに対応する要素の存在確認と、存在確認の操作処理をtestOperationListに追加します。

addConfirmOperationメソッドですが、
SelenideUtil.confirmExistenceByCssSelector(cssSelector!!)で実際の存在確認を行います。

SelenideUtil
        @JvmStatic
        fun confirmExistenceByCssSelector(cssSelector: String): Boolean {
            return try {
                Selenide.`$$`(By.cssSelector(cssSelector)).shouldHaveSize(1)
                true;
            } catch (e: Throwable) {
                false;
            }
        }

存在確認が成功すれば、確認処理の文字列を生成し、testOperationListに追加します。

OperationGenerator
        val confirmOperation =
            CommonDef.CONFIRM_EXISTENCE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
        testOperationList.add("//${elementName}の存在確認")
        testOperationList.add(confirmOperation)
        testOperationList.add("")
CommonDef
        const val TARGET_CSS_SELECTOR = "%targetCssSelector"
        const val CONFIRM_EXISTENCE_TEMPLATE = "SelenideUtil.confirmExistenceByCssSelector(\"$TARGET_CSS_SELECTOR\")"

ここまでが共通処理となります。

OperationGeneratorのテスト

OperationGeneratorインターフェースの実装クラスのテスト

OperationGeneratorインターフェースの実装クラスのテストクラスは以下のような構成となります。

testclass3.png

OperationGeneratorTestBase

SelenideUtilをmockkObjectにするメソッドとテスト対象の実装クラスを返却する抽象メソッドのgetTargetOperationGeneratorを含みます。

package jp.small_java_world.testopegen.generator

import io.mockk.mockkObject
import jp.small_java_world.testopegen.TestBase
import jp.small_java_world.testopegen.util.SelenideUtil

open abstract class OperationGeneratorTestBase : TestBase() {
    open fun beforeEach() = mockkObject(SelenideUtil)
    abstract fun getTargetOperationGenerator(): OperationGenerator
}

TestBase

モックをクリアするunmockkAll()
生成した操作処理のListを期待値を含んだファイルと比較するassertFileEqualsメソッド
を含んでいます。

package jp.small_java_world.testopegen

import io.mockk.unmockkAll
import org.apache.commons.io.FileUtils
import org.junit.jupiter.api.Assertions.assertEquals
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.nio.charset.StandardCharsets

open class TestBase {
    private val logger: Logger = LoggerFactory.getLogger(TestBase::class.java)

    open fun afterEach() = unmockkAll()

    fun assertFileEquals(expectedResultFileName: String, actual: List<String>) {
        logger.info("assertFileEquals expectedResultFileName={} start", expectedResultFileName)
        val expectedResult = readText(expectedResultFileName)
        val actualBuilder = StringBuilder()
        actual.forEach { actualBuilder.appendLine(it) }
        assertEquals(expectedResult, actualBuilder.toString())
    }

    /**
     * com.example.todoList.controller.TopControllerからfileName="expectedListResult.txt"
     * で呼び出された場合はbuildディレクトリの配下のclasses/kotlin/test/com/example/todoList/controller/expectedListResult.txt
     * を読み込んで内容の文字列を返却します。
     */
    private fun readText(fileName: String): String {
        val url = this.javaClass.getResource(".")
        val fileFullPath = url.path + File.separator + fileName
        val targetFile = File(fileFullPath)
        if (targetFile.exists()) {
            return FileUtils.readFileToString(targetFile, StandardCharsets.UTF_8);
        }
        return "not exist fileName:${targetFile.absolutePath}"
    }
}

TextOperationGenerator

まだ実装クラスが説明できていないのですが、TextOperationGeneratorが実装されている前提のOperationGeneratorのテストのを説明いたします。
要素の存在確認失敗時の処理は、各実装クラスで共通となりますので、OperationGenerator#generateOperationで`SelenideUtil.confirmExistenceByCssSelector(cssSelector!!)が失敗する場合のテストのみ実装します。

OperationGeneratorTest
package jp.small_java_world.testopegen.generator

import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.verify
import jp.small_java_world.testopegen.util.SelenideUtil
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class OperationGeneratorTest : OperationGeneratorTestBase() {
    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

    override fun getTargetOperationGenerator(): OperationGenerator {
        return TextOperationGenerator()
    }

    @Test
    fun testOperationGenerator() {
        val cssSelector = "cssSelectorValue"
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns false

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("confirmFail.txt", result)

        verify(exactly = 1) { SelenideUtil.confirmExistenceByCssSelector(cssSelector) }
    }
}
OperationGeneratorTest
    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

OperationGeneratorTestBaseopen fun beforeEach() = mockkObject(SelenideUtil)を呼び出しSelenideUtilをモック化しています。

OperationGeneratorTest
    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

TestBaseopen fun afterEach() = unmockkAll()を呼び出しSelenideUtilのモック化を解除しています。

OperationGeneratorTest
    override fun getTargetOperationGenerator(): OperationGenerator {
        return TextOperationGenerator()
    }

テスト対象はTextOperationGeneratorですので、TextOperationGeneratorのインスタンスをリターンします。

やっとテストメソッドです。

OperationGeneratorTest
    @Test
    fun testOperationGenerator() {
        val cssSelector = "cssSelectorValue"
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns false

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("confirmFail.txt", result)

        verify(exactly = 1) { SelenideUtil.confirmExistenceByCssSelector(cssSelector) }
    }

モック化されたSelenideUtilconfirmExistenceByCssSelector(cssSelector)の結果をfalseに固定し、
getTargetOperationGenerator().generateOperation(cssSelector)の結果が期待値ファイルの中身と一致するか検証しています。

confirmFail.txt
/**************** cssSelector cssSelectorValue の処理 start ****************/
//confirmExistenceByCssSelector fail

TextOperationGenerator

input textに対応した実装クラスとなります。

TextOperationGenerator
package jp.small_java_world.testopegen.generator

import jp.small_java_world.testopegen.define.CommonDef
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil
import jp.small_java_world.testopegen.util.generateRandomLetterOrDigit

class TextOperationGenerator : OperationGenerator {
    override fun getElementType(): TargetElementType {
        return TargetElementType.INPUT_TEXT
    }

    override fun generateCustomOperation(
        cssSelector: String?,
        testOperationList: MutableList<String>
    ): Collection<String> {
        val inputValue = generateRandomLetterOrDigit(4)

        testOperationList.add("//テキストボックスへの入力")
        SelenideUtil.inputTextByCssSelector(cssSelector!!, inputValue)

        val inputOperation = CommonDef.INPUT_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
            .replace(CommonDef.INPUT_VALUE, inputValue)
        testOperationList.add(inputOperation)
        testOperationList.add("")

        testOperationList.add("//テキストボックスへ入力した値の検証")
        SelenideUtil.shouldBeValueByCssSelector(cssSelector, inputValue)

        val confirmOperation = CommonDef.CONFIRM_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
            .replace(CommonDef.INPUT_VALUE, inputValue)
        testOperationList.add(confirmOperation)
        testOperationList.add("")

        return testOperationList;
    }
}

val inputValue = generateRandomLetterOrDigit(4)で長さが4のランダムな英数字の文字列を生成しています。
SelenideUtil.inputTextByCssSelector(cssSelector!!, inputValue)で実際に生成した文字列を入力しています。

SelenideUtil
        @JvmStatic
        fun inputTextByCssSelector(cssSelector: String, text: String) {
            selectByCssSelector(cssSelector).value = text
        }

生成した文字列を入力する操作をtestOperationListに追加しています。

TextOperationGenerator
        val inputOperation = CommonDef.INPUT_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
            .replace(CommonDef.INPUT_VALUE, inputValue)
        testOperationList.add(inputOperation)
        testOperationList.add("")
CommonDef
        const val TARGET_CSS_SELECTOR = "%targetCssSelector"
        const val INPUT_VALUE = "%inputTextValue"
        const val INPUT_VALUE_TEMPLATE =
            "SelenideUtil.inputTextByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"

入力した文字列が入力されていることを検証しています。

TextOperationGenerator
        SelenideUtil.shouldBeValueByCssSelector(cssSelector, inputValue)
SelenideUtil
        @JvmStatic
        fun shouldBeValueByCssSelector(cssSelector: String, expect: String) {
            selectByCssSelector(cssSelector).shouldBe(value(expect))
        }

入力した文字列が入力されていることを検証する操作をtestOperationListに追加しています。

TextOperationGenerator
        val confirmOperation = CommonDef.CONFIRM_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
            .replace(CommonDef.INPUT_VALUE, inputValue)
        testOperationList.add(confirmOperation)
        testOperationList.add("")
CommonDef
        const val CONFIRM_VALUE_TEMPLATE =
            "SelenideUtil.shouldBeValueByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"

generateRandomLetterOrDigitメソッドは以下の通りです。

TestUtil.kt
package jp.small_java_world.testopegen.util

import org.apache.commons.text.CharacterPredicate
import org.apache.commons.text.RandomStringGenerator

fun generateRandomLetterOrDigit(length: Int): String {
    return RandomStringGenerator.Builder().withinRange('0'.toInt(), 'z'.toInt())
        .filteredBy(CharacterPredicate { codePoint: Int ->
            Character.isLetterOrDigit(
                codePoint
            )
        })
        .build().generate(length)
}

TextOperationGeneratorの説明は以上です。

TextOperationGeneratorのテスト

TextOperationGeneratorTestは以下のようになります。

package jp.small_java_world.testopegen.generator

import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.verify
import jp.small_java_world.testopegen.util.SelenideUtil
import jp.small_java_world.testopegen.util.generateRandomLetterOrDigit
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class TextOperationGeneratorTest : OperationGeneratorTestBase() {
    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

    override fun getTargetOperationGenerator(): OperationGenerator {
        return TextOperationGenerator()
    }

    @Test
    fun testOperationGenerator() {
        val cssSelector = "cssSelectorValue"
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true

        val dummyStr = "dummy"
        mockkStatic("jp.small_java_world.testopegen.util.TestUtilKt")
        every { generateRandomLetterOrDigit(any<Int>()) } returns dummyStr
        every { SelenideUtil.inputTextByCssSelector(cssSelector, dummyStr) } returns Unit
        every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, dummyStr) } returns Unit

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("text-successResult.txt", result)

        verify(exactly = 1) { SelenideUtil.confirmExistenceByCssSelector(cssSelector) }

        verify(exactly = 1) {
            generateRandomLetterOrDigit(any<Int>())
            SelenideUtil.inputTextByCssSelector(cssSelector, dummyStr)
            SelenideUtil.shouldBeValueByCssSelector(cssSelector, dummyStr)
        }
    }
}
TextOperationGeneratorTest
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true

SelenideUtil.confirmExistenceByCssSelector(cssSelector)が失敗するテストケースは、OperationGeneratorTestで実装済みですので、
SelenideUtil.confirmExistenceByCssSelector(cssSelector)の結果がtrueになるとの振る舞いを定義しています。

TextOperationGeneratorTest
        val dummyStr = "dummy"
        mockkStatic("jp.small_java_world.testopegen.util.TestUtilKt")
        every { generateRandomLetterOrDigit(any<Int>()) } returns dummyStr

TextOperationGenerator#generateCustomOperationで、generateRandomLetterOrDigit(4)で生成した文字列を対象のテキストボックスに入力するので、この文字列を固定するために、TestUtil.ktをモック化しgenerateRandomLetterOrDigitの振る舞いを定義しています。

TestUtil.ktは以下のようになります。

TestUtil.kt
package jp.small_java_world.testopegen.util

import org.apache.commons.text.CharacterPredicate
import org.apache.commons.text.RandomStringGenerator

fun generateRandomLetterOrDigit(length: Int): String {
    return RandomStringGenerator.Builder().withinRange('0'.toInt(), 'z'.toInt())
        .filteredBy(CharacterPredicate { codePoint: Int ->
            Character.isLetterOrDigit(
                codePoint
            )
        })
        .build().generate(length)
}
TextOperationGeneratorTest
        every { SelenideUtil.inputTextByCssSelector(cssSelector, dummyStr) } returns Unit
        every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, dummyStr) } returns Unit

SelenideUtil.inputTextByCssSelectorSelenideUtil.shouldBeValueByCssSelectorが呼び出されたときの振る舞いを定義しています。

モックの準備が整ったので、OperationGenerator#generateOperationを呼び出し、生成された操作処理のList<String>が期待値ファイルの中身と一致することを検証します。

TextOperationGeneratorTest
        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("text-successResult.txt", result)
text-successResult.txt
/**************** cssSelector cssSelectorValue の処理 start ****************/
//テキストボックスの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")

//テキストボックスへの入力
SelenideUtil.inputTextByCssSelector("cssSelectorValue", "dummy")

//テキストボックスへ入力した値の検証
SelenideUtil.shouldBeValueByCssSelector("cssSelectorValue", "dummy")

最後にモックのverifyを実施します。

TextOperationGeneratorTest
        verify(exactly = 1) {
            SelenideUtil.confirmExistenceByCssSelector(cssSelector)
            generateRandomLetterOrDigit(any<Int>())
            SelenideUtil.inputTextByCssSelector(cssSelector, dummyStr)
            SelenideUtil.shouldBeValueByCssSelector(cssSelector, dummyStr)
        }

ButtonOperationGenerator

input buttonの操作処理を生成するクラスとなります。

ButtonOperationGenerator
package jp.small_java_world.testopegen.generator

import com.codeborne.selenide.ex.UIAssertionError
import jp.small_java_world.testopegen.define.CommonDef
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil

class ButtonOperationGenerator : OperationGenerator {
    override fun getElementType(): TargetElementType {
        return TargetElementType.INPUT_BUTTON
    }

    override fun generateCustomOperation(
        cssSelector: String?,
        testOperationList: MutableList<String>
    ): Collection<String> {
        var usingJavaScript = false

        try {
            SelenideUtil.clickByCssSelector(cssSelector!!)
        } catch (e: Throwable) {
            when (e) {
                //UIAssertionError:非表示の要素をクリック、InvalidStateException他の要素によりクリックがブロック
                is UIAssertionError -> {
                    testOperationList.add("// clickByCssSelector fail")
                    usingJavaScript = true
                }
                else -> throw e
            }
        }

        val clickOperation = CommonDef.CLICK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
        testOperationList.add(if (usingJavaScript) "//$clickOperation" else clickOperation)
        testOperationList.add("")

        if (usingJavaScript) {
            val clickUseJsOperation =
                CommonDef.CLICK_USE_JS_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
            testOperationList.add(clickUseJsOperation)
            testOperationList.add("")
        }

        return testOperationList;
    }
}

「課題の解決策」のところでも記載したのですが、ボタンは実際にクリックして、押せない場合はJavaScriptでクリックするようになります。

ButtonOperationGenerator
        var usingJavaScript = false

        try {
            SelenideUtil.clickByCssSelector(cssSelector!!)
        } catch (e: Throwable) {
            when (e) {
                //UIAssertionError:非表示の要素をクリック、InvalidStateException他の要素によりクリックがブロック
                is UIAssertionError -> {
                    testOperationList.add("// clickByCssSelector fail")
                    usingJavaScript = true
                }
                else -> throw e
            }
        }

usingJavaScript=trueの場合は通常クリック処理はコメントとして生成します。

ButtonOperationGenerator
        val clickOperation = CommonDef.CLICK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
        testOperationList.add(if (usingJavaScript) "//$clickOperation" else clickOperation)
        testOperationList.add("")
CommonDef
        const val CLICK_TEMPLATE = "SelenideUtil.clickByCssSelector(\"$TARGET_CSS_SELECTOR\")"

繰り返しになりますが、usingJavaScript=trueの場合はJavaScriptでクリックする操作を生成します。

ButtonOperationGenerator
        if (usingJavaScript) {
            val clickUseJsOperation =
                CommonDef.CLICK_USE_JS_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
            testOperationList.add(clickUseJsOperation)
            testOperationList.add("")
        }
CommonDef
        const val CLICK_USE_JS_TEMPLATE = "SelenideUtil.clickByCssSelectorUseJS(\"$TARGET_CSS_SELECTOR\")"
SelenideUtil
        @JvmStatic
        fun clickByCssSelectorUseJS(cssSelector: String) {
            val driver = WebDriverRunner.getWebDriver()
            val executor = driver as JavascriptExecutor
            val element = selectByCssSelector(cssSelector)
            executor.executeScript("arguments[0].click()", element)
        }

ButtonOperationGeneratorのテスト

ButtonOperationGeneratorTest
package jp.small_java_world.testopegen.generator

import com.codeborne.selenide.ex.InvalidStateException
import io.mockk.every
import io.mockk.verify
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

class ButtonOperationGeneratorTest : OperationGeneratorTestBase() {
    companion object {
        val EXPECTED_FILENAME_PREFIX = TargetElementType.INPUT_BUTTON.type
    }

    enum class ButtonOperationGeneratorTestType(val id: Int, val resultFileName: String) {
        UIAssertionError(200, "$EXPECTED_FILENAME_PREFIX-successJsResult.txt"),
        Success(300, "$EXPECTED_FILENAME_PREFIX-successResult.txt"),
    }

    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

    override fun getTargetOperationGenerator(): OperationGenerator {
        return ButtonOperationGenerator()
    }

    @ParameterizedTest
    @EnumSource(ButtonOperationGeneratorTestType::class)
    fun testButtonOperationGenerator(testType: ButtonOperationGeneratorTestType) {
        val cssSelector = "cssSelectorValue"

        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true
        if (testType == ButtonOperationGeneratorTestType.Success) {
            every { SelenideUtil.clickByCssSelector(cssSelector) } returns Unit
        } else {
            //InvalidStateExceptionはUIAssertionErrorの継承クラス
            every { SelenideUtil.clickByCssSelector(cssSelector) } throws InvalidStateException(null, "")
        }

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals(testType.resultFileName, result)

        verify(exactly = 1) {
            SelenideUtil.confirmExistenceByCssSelector(cssSelector)
            SelenideUtil.clickByCssSelector(
                cssSelector
            )
        }
    }
}

通常クリックとUIAssertionErrorが発生してJavaScriptで押すとの2パターンのテストとなりますので、この2パターンをenumのButtonOperationGeneratorTestTypeで表現しています。生成された操作の期待値ファイルもButtonOperationGeneratorTestTypeに含んでいます。

ButtonOperationGeneratorTest
    companion object {
        val EXPECTED_FILENAME_PREFIX = TargetElementType.INPUT_BUTTON.type
    }

    enum class ButtonOperationGeneratorTestType(val id: Int, val resultFileName: String) {
        UIAssertionError(200, "$EXPECTED_FILENAME_PREFIX-successJsResult.txt"),
        Success(300, "$EXPECTED_FILENAME_PREFIX-successResult.txt"),
    }

ButtonOperationGeneratorTestTypeでモックの振る舞いを分岐して、操作処理の生成結果を期待値のファイルと比較します。

ButtonOperationGeneratorTest
        @ParameterizedTest
        @EnumSource(ButtonOperationGeneratorTestType::class)
        fun testButtonOperationGenerator(testType: ButtonOperationGeneratorTestType) {
            val cssSelector = "cssSelectorValue"

            every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true
            if (testType == ButtonOperationGeneratorTestType.Success) {
                every { SelenideUtil.clickByCssSelector(cssSelector) } returns Unit
            } else {
                //InvalidStateExceptionはUIAssertionErrorの継承クラス
                every { SelenideUtil.clickByCssSelector(cssSelector) } throws InvalidStateException(null, "")
            }

            var result = getTargetOperationGenerator().generateOperation(cssSelector)
            assertFileEquals(testType.resultFileName, result)

            verify(exactly = 1) {
                SelenideUtil.confirmExistenceByCssSelector(cssSelector)
                SelenideUtil.clickByCssSelector(
                    cssSelector
                )
            }
        }

期待値のファイルは以下の通りです。

button-successJsResult.txt
/**************** cssSelector cssSelectorValue の処理 start ****************/
//ボタンの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")

// clickByCssSelector fail
//SelenideUtil.clickByCssSelector("cssSelectorValue")

SelenideUtil.clickByCssSelectorUseJS("cssSelectorValue")


button-successResult.txt
/**************** cssSelector cssSelectorValue の処理 start ****************/
//ボタンの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")

SelenideUtil.clickByCssSelector("cssSelectorValue")


CheckOperationGenerator

チェックボックスの操作処理は

  • チェックボックスのチェック
  • チェックされたことの検証
  • チェックボックスのアンチェック
  • チェックさていないことの検証

となります。


package jp.small_java_world.testopegen.generator

import jp.small_java_world.testopegen.define.CommonDef
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil

class CheckOperationGenerator : OperationGenerator {
    override fun getElementType(): TargetElementType {
        return TargetElementType.INPUT_CHECKBOX
    }

    override fun generateCustomOperation(
        cssSelector: String?,
        testOperationList: MutableList<String>
    ): Collection<String> {
        testOperationList.add("//チェックボックスのチェック")
        SelenideUtil.checkByCssSelector(cssSelector!!)

        val checkOperation = CommonDef.CHECK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(checkOperation)
        testOperationList.add("")

        testOperationList.add("//チェックされたことの検証")
        SelenideUtil.shouldBeSelectedByCssSelector(cssSelector!!)

        val confirmOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(confirmOperation)
        testOperationList.add("")

        testOperationList.add("//チェックボックスのアンチェック")
        SelenideUtil.unCheckByCssSelector(cssSelector)

        val unCheckOperation = CommonDef.UNCHECK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(unCheckOperation)
        testOperationList.add("")

        testOperationList.add("//チェックさていないことの検証")
        SelenideUtil.shouldBeNotSelectedByCssSelector(cssSelector)

        val confirmUnCheckOperation =
            CommonDef.CONFIRM_NOT_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(confirmUnCheckOperation)
        testOperationList.add("")

        return testOperationList;
    }
}

チェックボックスのチェック操作を生成します。

CheckOperationGenerator
        val checkOperation = CommonDef.CHECK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(checkOperation)
        testOperationList.add("")
CommonDef
        const val CHECK_TEMPLATE = "SelenideUtil.checkByCssSelector(\"$TARGET_CSS_SELECTOR\")"
SelenideUtil
        @JvmStatic
        fun checkByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).isSelected = true
        }

チェックボックスがチェックされていることの検証操作を生成します。

CheckOperationGenerator
        val confirmOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(confirmOperation)
        testOperationList.add("")
CommonDef
        const val CONFIRM_SELECTED_TEMPLATE = "SelenideUtil.shouldBeSelectedByCssSelector(\"$TARGET_CSS_SELECTOR\")"
SelenideUtil
        @JvmStatic
        fun shouldBeSelectedByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).shouldBe(selected)
        }

チェックボックスのチェックをはずす操作を生成します。

CheckOperationGenerator
        val unCheckOperation = CommonDef.UNCHECK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(unCheckOperation)
        testOperationList.add("")
CommonDef
        const val UNCHECK_TEMPLATE = "SelenideUtil.unCheckByCssSelector(\"$TARGET_CSS_SELECTOR\")"
SelenideUtil
        @JvmStatic
        fun unCheckByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).isSelected = false
        }

チェックボックスがチェックされていないことの検証操作を生成します。

CheckOperationGenerator
        val confirmUnCheckOperation =
            CommonDef.CONFIRM_NOT_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(confirmUnCheckOperation)
        testOperationList.add("")
CommonDef
        const val CONFIRM_NOT_SELECTED_TEMPLATE =
            "SelenideUtil.shouldBeNotSelectedByCssSelector(\"$TARGET_CSS_SELECTOR\")"
SelenideUtil
        @JvmStatic
        fun shouldBeNotSelectedByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).shouldNotBe(selected)
        }

CheckOperationGeneratorのテスト

CheckOperationGeneratorTestは以下のようになります。

package jp.small_java_world.testopegen.generator

import io.mockk.every
import io.mockk.verify
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

class CheckOperationGeneratorTest : OperationGeneratorTestBase() {
    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

    override fun getTargetOperationGenerator(): OperationGenerator {
        return CheckOperationGenerator()
    }

    @Test
    fun testOperationGenerator() {
        val cssSelector = "cssSelectorValue"
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true
        every { SelenideUtil.checkByCssSelector(cssSelector) } returns Unit
        every { SelenideUtil.shouldBeSelectedByCssSelector(cssSelector) } returns Unit
        every { SelenideUtil.unCheckByCssSelector(cssSelector) } returns Unit
        every { SelenideUtil.shouldBeNotSelectedByCssSelector(cssSelector) } returns Unit

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("checkbox-successResult.txt", result)

        verify(exactly = 1) {
            SelenideUtil.confirmExistenceByCssSelector(cssSelector)
            SelenideUtil.checkByCssSelector(cssSelector)
            SelenideUtil.shouldBeSelectedByCssSelector(cssSelector)
            SelenideUtil.unCheckByCssSelector(cssSelector)
            SelenideUtil.shouldBeNotSelectedByCssSelector(cssSelector)
        }
    }
}

RadioOperationGenerator

ラジオボタンの操作処理は

  • cssSelectorからname属性の値の取得
  • cssSelectorからvalue属性の値の取得
  • nameとvalueを指定しての選択
  • 選択されていることの検証

となります。

package jp.small_java_world.testopegen.generator

import jp.small_java_world.testopegen.define.CommonDef
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil

class RadioOperationGenerator : OperationGenerator {
    override fun getElementType(): TargetElementType {
        return TargetElementType.INPUT_RADIO
    }

    override fun generateCustomOperation(
        cssSelector: String?,
        testOperationList: MutableList<String>
    ): Collection<String> {
        var radioName = SelenideUtil.getNameByCssSelector(cssSelector!!)
        var radioValue = SelenideUtil.getValueByCssSelector(cssSelector)

        testOperationList.add("//ラジオボタンの選択")
        SelenideUtil.selectRadioByCssSelector("input[name=$radioName]", radioValue!!)

        var selectOperation =
            CommonDef.SELECT_RADIO_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, "input[name=$radioName]")
                .replace(CommonDef.INPUT_VALUE, radioValue)
        testOperationList.add(selectOperation)
        testOperationList.add("")

        testOperationList.add("//ラジオボタンの選択の検証")
        SelenideUtil.shouldBeSelectedByCssSelector(cssSelector)

        val confirmOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(confirmOperation)
        testOperationList.add("")

        return testOperationList;
    }
}

まずは、nameとvalueの属性値を取得します。

RadioOperationGenerator
        var radioName = SelenideUtil.getNameByCssSelector(cssSelector!!)
        var radioValue = SelenideUtil.getValueByCssSelector(cssSelector)
RadioOperationGenerator
        @JvmStatic
        fun getValueByCssSelector(cssSelector: String): String? {
            return selectByCssSelector(cssSelector).value
        }

        @JvmStatic
        fun getNameByCssSelector(cssSelector: String): String? {
            return selectByCssSelector(cssSelector).getAttribute("name")
        }

次に、実際にラジオボタンを選択します。

RadioOperationGenerator
        SelenideUtil.selectRadioByCssSelector("input[name=$radioName]", radioValue!!)
SelenideUtil
        @JvmStatic
        fun selectRadioByCssSelector(cssSelector: String, value: String) {
            selectByCssSelector(cssSelector).selectRadio(value)
        }

ラジオボタンの選択操作を生成します。

RadioOperationGenerator
        var selectOperation =
            CommonDef.SELECT_RADIO_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, "input[name=$radioName]")
                .replace(CommonDef.INPUT_VALUE, radioValue)
        testOperationList.add(selectOperation)
        testOperationList.add("")
CommonDef
        const val SELECT_RADIO_TEMPLATE =
            "SelenideUtil.selectRadioByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"

実際にラジオボタンが選択されていることを検証します。

RadioOperationGenerator
        SelenideUtil.shouldBeSelectedByCssSelector(cssSelector)
SelenideUtil
        @JvmStatic
        fun shouldBeSelectedByCssSelector(cssSelector: String) {
            selectByCssSelector(cssSelector).shouldBe(selected)
        }

ラジオボタンが選択されていることを検証のを行う操作を生成します。

RadioOperationGenerator
        val confirmOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
        testOperationList.add(confirmOperation)
        testOperationList.add("")
CommonDef
        const val CONFIRM_SELECTED_TEMPLATE = "SelenideUtil.shouldBeSelectedByCssSelector(\"$TARGET_CSS_SELECTOR\")"

RadioOperationGeneratorのテスト

RadioOperationGeneratorTest は以下のようになります。
新たな要素は含まれておりませんので、説明は不要と思います。

package jp.small_java_world.testopegen.generator

import io.mockk.every
import io.mockk.verify
import jp.small_java_world.testopegen.util.SelenideUtil
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class RadioOperationGeneratorTest : OperationGeneratorTestBase() {
    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

    override fun getTargetOperationGenerator(): OperationGenerator {
        return RadioOperationGenerator()
    }

    @Test
    fun testOperationGenerator() {
        val cssSelector = "cssSelectorValue"
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true

        val radioName = "radio-name"
        val radioValue = "radio-value"

        every { SelenideUtil.getNameByCssSelector(cssSelector) } returns radioName
        every { SelenideUtil.getValueByCssSelector(cssSelector) } returns radioValue
        every { SelenideUtil.selectRadioByCssSelector("input[name=$radioName]", radioValue) } returns Unit
        every { SelenideUtil.shouldBeSelectedByCssSelector(cssSelector) } returns Unit

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("radio-successResult.txt", result)

        verify(exactly = 1) {
            SelenideUtil.confirmExistenceByCssSelector(cssSelector)
            SelenideUtil.getNameByCssSelector(cssSelector)
            SelenideUtil.getValueByCssSelector(cssSelector)
            SelenideUtil.selectRadioByCssSelector("input[name=$radioName]", radioValue)
            SelenideUtil.shouldBeSelectedByCssSelector(
                cssSelector
            )
        }
    }
}

radio-successResult.txtは以下のようになります。

radio-successResult.txt
/**************** cssSelector cssSelectorValue の処理 start ****************/
//ラジオボタンの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")

//ラジオボタンの選択
SelenideUtil.selectRadioByCssSelector("input[name=radio-name]", "radio-value")

//ラジオボタンの選択の検証
SelenideUtil.shouldBeSelectedByCssSelector("cssSelectorValue")

SelectOperationGenerator

セレクトボックスの操作処理は

  • "$cssSelector > option"で対象HTMの要素のElementsCollectionを取得
  • ElementsCollectionをループし、各oprionにを対象として処理を実施
  • optionに対応するSELECTボックスを値で選択
  • optionに対応するSELECTボックスをoptionで選択
  • optionに対応するSELECTボックスの選択確認

となります。

SelectOperationGenerator
package jp.small_java_world.testopegen.generator

import jp.small_java_world.testopegen.define.CommonDef
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.util.SelenideUtil

class SelectOperationGenerator : OperationGenerator {
    override fun getElementType(): TargetElementType {
        return TargetElementType.SELECT
    }

    override fun generateCustomOperation(
        cssSelector: String?,
        testOperationList: MutableList<String>
    ): Collection<String> {
        val options = SelenideUtil.selectListByCssSelector("$cssSelector > option")

        for (option in options) {
            SelenideUtil.selectOptionByValueByCssSelector(cssSelector!!, option.value!!)
            SelenideUtil.selectOptionByCssSelector(cssSelector!!, option.text())
            SelenideUtil.shouldBeValueByCssSelector(cssSelector!!, option.value!!)

            testOperationList.add("//SELECTボックスを値で選択 value=$option.value")
            val selectOptionByValueOperation =
                CommonDef.SELECT_OPTION_BY_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
                    .replace(CommonDef.INPUT_VALUE, option.value!!)
            testOperationList.add(selectOptionByValueOperation)
            testOperationList.add("")

            testOperationList.add("//SELECTボックスをオプションで選択 option=${option.text()}")
            val selectOptionOperation =
                CommonDef.SELECT_SELECT_OPTION_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
                    .replace(CommonDef.INPUT_VALUE, option.text())
            testOperationList.add(selectOptionOperation)
            testOperationList.add("")

            testOperationList.add("//SELECTボックスの選択確認")
            val confirmSelectOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(
                CommonDef.TARGET_CSS_SELECTOR,
                "$cssSelector > option[value='${option.value!!}']"
            ).replace(CommonDef.INPUT_VALUE, option.text())
            testOperationList.add(confirmSelectOperation)
            testOperationList.add("")
        }

        return testOperationList;
    }
}

以下のhtmlを処理対とした場合は、
ラジオボタンの一意識別子であるcssSelectorは、select[name='blood']となります。

  <select name="blood">
    <option value="A">A型</option>
    <option value="B">B型</option>
  </select>

select[name='blood']配下のoptionを取得します。

SelectOperationGenerator
        val options = SelenideUtil.selectListByCssSelector("$cssSelector > option")

以降は、optionsの各要素に対する処理となります。

SelectOperationGenerator
        for (option in options) {

まずは、実際にセレクトボックスを値で選択します。

SelectOperationGenerator
            SelenideUtil.selectOptionByValueByCssSelector(cssSelector!!, option.value!!)
SelenideUtil
        @JvmStatic
        fun selectOptionByValueByCssSelector(cssSelector: String, value: String) {
            selectByCssSelector(cssSelector).selectOptionByValue(value)
        }

次に実際にセレクトボックスをオプション(text)で選択します。

SelectOperationGenerator
            SelenideUtil.selectOptionByCssSelector(cssSelector!!, option.text())
SelenideUtil
        @JvmStatic
        fun selectOptionByCssSelector(cssSelector: String, value: String) {
            selectByCssSelector(cssSelector).selectOption(value)
        }

引き続き、実際にセレクトボックスの選択確認を行います。

SelectOperationGenerator
            SelenideUtil.shouldBeValueByCssSelector(cssSelector!!, option.text())
SelenideUtil
        @JvmStatic
        fun shouldBeValueByCssSelector(cssSelector: String, expect: String) {
            selectByCssSelector(cssSelector).shouldBe(value(expect))
        }

これ以降は、前述の実際の操作に対応する操作処理の生成となります。

セレクトボックスを値で選択する操作の生成

SelectOperationGenerator
            testOperationList.add("//SELECTボックスを値で選択 value=$option.value")
            val selectOptionByValueOperation =
                CommonDef.SELECT_OPTION_BY_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
                    .replace(CommonDef.INPUT_VALUE, option.value!!)
            testOperationList.add(selectOptionByValueOperation)
            testOperationList.add("")
CommonDef
        const val SELECT_OPTION_BY_VALUE_TEMPLATE =
            "SelenideUtil.selectOptionByValueByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"

セレクトボックスをオプション(text)で選択する操作の生成

SelectOperationGenerator
            testOperationList.add("//SELECTボックスをオプションで選択 option=${option.text()}")
            val selectOptionOperation =
                CommonDef.SELECT_SELECT_OPTION_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
                    .replace(CommonDef.INPUT_VALUE, option.text())
            testOperationList.add(selectOptionOperation)
            testOperationList.add("")
CommonDef
        const val SELECT_SELECT_OPTION_TEMPLATE =
            "SelenideUtil.selectOptionByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"

セレクトボックスの選択確認の操作の生成

SelectOperationGenerator
            testOperationList.add("//SELECTボックスの選択確認")
            val confirmSelectOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(
                CommonDef.TARGET_CSS_SELECTOR,
                "$cssSelector > option[value='${option.value!!}']"
            ).replace(CommonDef.INPUT_VALUE, option.text())
            testOperationList.add(confirmSelectOperation)
            testOperationList.add("")
CommonDef
        const val CONFIRM_SELECTED_TEMPLATE = "SelenideUtil.shouldBeSelectedByCssSelector(\"$TARGET_CSS_SELECTOR\")"

SelectOperationGeneratorのテスト

SelectOperationGeneratorTest は以下のようになります。

package jp.small_java_world.testopegen.generator

import com.codeborne.selenide.ElementsCollection
import com.codeborne.selenide.impl.StaticDriver
import com.codeborne.selenide.impl.WebElementsCollectionWrapper
import io.mockk.every
import io.mockk.verify
import jp.small_java_world.testopegen.element.DummySelenideElement
import jp.small_java_world.testopegen.util.SelenideUtil
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class SelectOperationGeneratorTest : OperationGeneratorTestBase() {
    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }

    override fun getTargetOperationGenerator(): OperationGenerator {
        return SelectOperationGenerator()
    }

    @Test
    fun testOperationGenerator() {
        val cssSelector = "cssSelectorValue"
        every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true

        val value1 = "value1"
        val value2 = "value2"
        val text1 = "text1"
        val text2 = "text2"

        var elementsCollectionValue = mutableListOf(DummySelenideElement(), DummySelenideElement())
        elementsCollectionValue[0].textValue = text1
        elementsCollectionValue[0].valueValue = value1
        elementsCollectionValue[1].textValue = text2
        elementsCollectionValue[1].valueValue = value2

        val optionCollection =
            WebElementsCollectionWrapper(StaticDriver(), elementsCollectionValue)

        val optionList = ElementsCollection(optionCollection)
        every { SelenideUtil.selectListByCssSelector("$cssSelector > option") } returns optionList
        every { SelenideUtil.selectOptionByValueByCssSelector(cssSelector, value1) } returns Unit
        every { SelenideUtil.selectOptionByCssSelector(cssSelector, text1) } returns Unit
        every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, value1) } returns Unit
        every { SelenideUtil.selectOptionByValueByCssSelector(cssSelector, value2) } returns Unit
        every { SelenideUtil.selectOptionByCssSelector(cssSelector, text2) } returns Unit
        every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, value2) } returns Unit

        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("select-successResult.txt", result)

        verify(exactly = 1) {
            SelenideUtil.confirmExistenceByCssSelector(cssSelector)
        }

        for (testDataPair in listOf(Pair(value1, text1), Pair(value2, text2))) {
            verify(exactly = 1) {
                SelenideUtil.confirmExistenceByCssSelector(cssSelector)
                SelenideUtil.selectListByCssSelector("$cssSelector > option")
                SelenideUtil.selectOptionByValueByCssSelector(cssSelector, testDataPair.first)
                SelenideUtil.selectOptionByCssSelector(cssSelector, testDataPair.second)
                SelenideUtil.shouldBeValueByCssSelector(cssSelector, testDataPair.first)
            }
        }
    }
}

SelenideUtil.selectListByCssSelector("$cssSelector > option")の結果を制御するためにDummySelenideElementクラスを作成しています。

DummySelenideElementは以下のようになります。com.codeborne.selenide.SelenideElementインターフェースの実装メソッドは、ほぼTODO("Not yet implemented sendKeys")みたいな実装で、必要なメソッドのみテストで振る舞いを定義可能なように実装しております。

package jp.small_java_world.testopegen.element

import com.codeborne.selenide.*
import com.codeborne.selenide.files.FileFilter
import org.openqa.selenium.*
import org.openqa.selenium.interactions.Coordinates
import java.awt.image.BufferedImage
import java.io.File
import java.time.Duration

class DummySelenideElement : SelenideElement {
    lateinit var textValue: String
    lateinit var valueValue: String

    override fun findElements(by: By?): MutableList<WebElement> {
        return mutableListOf()
    }

    override fun findElement(by: By?): WebElement {
        return DummySelenideElement()
    }

    override fun <X : Any?> getScreenshotAs(target: OutputType<X>): X {
        TODO("Not yet implemented getScreenshotAs")
    }

    override fun click(clickOption: ClickOptions) {
        TODO("Not yet implemented click clickOption")
    }

    override fun click() {
        TODO("Not yet implemented click")
    }

    override fun click(offsetX: Int, offsetY: Int) {
        TODO("Not yet implemented offsetX offsetY")
    }

    override fun submit() {
        TODO("Not yet implemented submit")
    }

    override fun sendKeys(vararg keysToSend: CharSequence?) {
        TODO("Not yet implemented sendKeys")
    }

    override fun clear() {
        TODO("Not yet implemented")
    }

    override fun getTagName(): String {
        return "dummyTagName"
    }

    override fun getAttribute(name: String): String? {
        return when (name) {
            "value" -> valueValue
            else -> ""
        }
    }

    override fun isSelected(): Boolean {
        return false
    }

    override fun isEnabled(): Boolean {
        TODO("Not yet implemented isEnabled")
    }

    override fun getText(): String {
        return textValue
    }

    override fun isDisplayed(): Boolean {
        return true
    }

    override fun getLocation(): Point {
        TODO("Not yet implemented getLocation")
    }

    override fun getSize(): Dimension {
        TODO("Not yet implemented getSize")
    }

    override fun getRect(): Rectangle {
        TODO("Not yet implemented getRect")
    }

    override fun getCssValue(propertyName: String): String {
        TODO("Not yet implemented getCssValue")
    }

    override fun getWrappedDriver(): WebDriver {
        TODO("Not yet implemented getWrappedDriver")
    }

    override fun getWrappedElement(): WebElement {
        return DummySelenideElement()
    }

    override fun getCoordinates(): Coordinates {
        TODO("Not yet implemented getCoordinates")
    }

    override fun getId(): String {
        TODO("Not yet implemented getId")
    }

    override fun setValue(text: String?): SelenideElement {
        TODO("Not yet implemented setValue")
    }

    override fun `val`(text: String?): SelenideElement {
        TODO("Not yet implemented val text")
    }

    override fun `val`(): String? {
        TODO("Not yet implemented val")
    }

    override fun append(text: String): SelenideElement {
        TODO("Not yet implemented append")
    }

    override fun pressEnter(): SelenideElement {
        TODO("Not yet implemented pressEnter")
    }

    override fun pressTab(): SelenideElement {
        TODO("Not yet implemented pressTab")
    }

    override fun pressEscape(): SelenideElement {
        TODO("Not yet implemented pressEscape")
    }

    override fun getAlias(): String? {
        TODO("Not yet implemented getAlias")
    }

    override fun text(): String {
        TODO("Not yet implemented text")
    }

    override fun getOwnText(): String {
        TODO("Not yet implemented getOwnText")
    }

    override fun innerText(): String {
        TODO("Not yet implemented innerText")
    }

    override fun innerHtml(): String {
        TODO("Not yet implemented innerHtml")
    }

    override fun attr(attributeName: String): String? {
        TODO("Not yet implemented attr")
    }

    override fun name(): String? {
        TODO("Not yet implemented name")
    }

    override fun getValue(): String? {
        return valueValue
    }

    override fun pseudo(pseudoElementName: String, propertyName: String): String {
        TODO("Not yet implemented pseudo1")
    }

    override fun pseudo(pseudoElementName: String): String {
        TODO("Not yet implemented pseudo2")
    }

    override fun selectRadio(value: String): SelenideElement {
        TODO("Not yet implemented selectRadio")
    }

    override fun data(dataAttributeName: String): String? {
        TODO("Not yet implemented data")
    }

    override fun exists(): Boolean {
        TODO("Not yet implemented exists")
    }

    override fun `is`(condition: Condition): Boolean {
        TODO("Not yet implemented is")
    }

    override fun has(condition: Condition): Boolean {
        TODO("Not yet implemented has")
    }

    override fun setSelected(selected: Boolean): SelenideElement {
        TODO("Not yet implemented setSelected")
    }

    override fun should(vararg condition: Condition?): SelenideElement {
        TODO("Not yet implemented should")
    }

    override fun should(condition: Condition, timeout: Duration): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun shouldHave(vararg condition: Condition?): SelenideElement {
        TODO("Not yet implemented shouldHave1")
    }

    override fun shouldHave(condition: Condition, timeout: Duration): SelenideElement {
        TODO("Not yet implemented shouldHave2")
    }

    override fun shouldBe(vararg condition: Condition?): SelenideElement {
        TODO("Not yet implemented shouldBe1")
    }

    override fun shouldBe(condition: Condition, timeout: Duration): SelenideElement {
        TODO("Not yet implemented shouldBe2")
    }

    override fun shouldNot(vararg condition: Condition?): SelenideElement {
        TODO("Not yet implemented shouldNot1")
    }

    override fun shouldNot(condition: Condition, timeout: Duration): SelenideElement {
        TODO("Not yet implemented shouldNot2")
    }

    override fun shouldNotHave(vararg condition: Condition?): SelenideElement {
        TODO("Not yet implemented shouldNotHave1")
    }

    override fun shouldNotHave(condition: Condition, timeout: Duration): SelenideElement {
        TODO("Not yet implemented shouldNotHave2")
    }

    override fun shouldNotBe(vararg condition: Condition?): SelenideElement {
        TODO("Not yet implemented shouldNotBe1")
    }

    override fun shouldNotBe(condition: Condition, timeout: Duration): SelenideElement {
        TODO("Not yet implemented shouldNotBe2")
    }

    override fun waitUntil(condition: Condition, timeoutMilliseconds: Long): SelenideElement {
        TODO("Not yet implemented waitUntil2")
    }

    override fun waitUntil(
        condition: Condition,
        timeoutMilliseconds: Long,
        pollingIntervalMilliseconds: Long
    ): SelenideElement {
        TODO("Not yet implemented waitUntil2")
    }

    override fun waitWhile(condition: Condition, timeoutMilliseconds: Long): SelenideElement {
        TODO("Not yet implemented waitUntil3")
    }

    override fun waitWhile(
        condition: Condition,
        timeoutMilliseconds: Long,
        pollingIntervalMilliseconds: Long
    ): SelenideElement {
        TODO("Not yet implemented waitUntil4")
    }

    override fun `as`(alias: String): SelenideElement {
        TODO("Not yet implemented as")
    }

    override fun parent(): SelenideElement {
        TODO("Not yet implemented parent")
    }

    override fun sibling(index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun preceding(index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun lastChild(): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun closest(tagOrClass: String): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun find(cssSelector: String): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun find(cssSelector: String, index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun find(selector: By): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun find(selector: By, index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun `$`(cssSelector: String): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun `$`(cssSelector: String, index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun `$`(selector: By): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun `$`(selector: By, index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun `$x`(xpath: String): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun `$x`(xpath: String, index: Int): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun findAll(cssSelector: String): ElementsCollection {
        TODO("Not yet implemented")
    }

    override fun findAll(selector: By): ElementsCollection {
        TODO("Not yet implemented")
    }

    override fun `$$`(cssSelector: String): ElementsCollection {
        TODO("Not yet implemented")
    }

    override fun `$$`(selector: By): ElementsCollection {
        TODO("Not yet implemented")
    }

    override fun `$$x`(xpath: String): ElementsCollection {
        TODO("Not yet implemented")
    }

    override fun uploadFromClasspath(vararg fileName: String?): File {
        TODO("Not yet implemented")
    }

    override fun uploadFile(vararg file: File?): File {
        TODO("Not yet implemented")
    }

    override fun selectOption(vararg index: Int) {
        TODO("Not yet implemented")
    }

    override fun selectOption(vararg text: String?) {
        TODO("Not yet implemented")
    }

    override fun selectOptionContainingText(text: String) {
        TODO("Not yet implemented")
    }

    override fun selectOptionByValue(vararg value: String?) {
        TODO("Not yet implemented")
    }

    override fun getSelectedOption(): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun getSelectedOptions(): ElementsCollection {
        TODO("Not yet implemented")
    }

    override fun getSelectedValue(): String? {
        TODO("Not yet implemented")
    }

    override fun getSelectedText(): String {
        TODO("Not yet implemented")
    }

    override fun scrollTo(): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun scrollIntoView(alignToTop: Boolean): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun scrollIntoView(scrollIntoViewOptions: String): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun download(): File {
        TODO("Not yet implemented")
    }

    override fun download(timeout: Long): File {
        TODO("Not yet implemented")
    }

    override fun download(fileFilter: FileFilter): File {
        TODO("Not yet implemented")
    }

    override fun download(timeout: Long, fileFilter: FileFilter): File {
        TODO("Not yet implemented")
    }

    override fun download(options: DownloadOptions): File {
        TODO("Not yet implemented")
    }

    override fun getSearchCriteria(): String {
        TODO("Not yet implemented")
    }

    override fun toWebElement(): WebElement {
        TODO("Not yet implemented")
    }

    override fun contextClick(): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun doubleClick(): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun hover(): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun dragAndDropTo(targetCssSelector: String): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun dragAndDropTo(target: WebElement): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun dragAndDropTo(targetCssSelector: String, options: DragAndDropOptions): SelenideElement {
        TODO("Not yet implemented")
    }

    override fun <ReturnType : Any?> execute(command: Command<ReturnType>): ReturnType {
        TODO("Not yet implemented")
    }

    override fun isImage(): Boolean {
        TODO("Not yet implemented")
    }

    override fun screenshot(): File? {
        TODO("Not yet implemented")
    }

    override fun screenshotAsImage(): BufferedImage? {
        TODO("Not yet implemented")
    }
}

まずは、`SelenideUtil.selectListByCssSelector("$cssSelector > option")`の戻り値を生成します。

```kotlin:SelectOperationGeneratorTest
        val value1 = "value1"
        val value2 = "value2"
        val text1 = "text1"
        val text2 = "text2"

        var elementsCollectionValue = mutableListOf(DummySelenideElement(), DummySelenideElement())
        elementsCollectionValue[0].textValue = text1
        elementsCollectionValue[0].valueValue = value1
        elementsCollectionValue[1].textValue = text2
        elementsCollectionValue[1].valueValue = value2

        val optionCollection =
            WebElementsCollectionWrapper(StaticDriver(), elementsCollectionValue)

        val optionList = ElementsCollection(optionCollection)

SelenideUtil.selectListByCssSelector("$cssSelector > option")の結果がoptionListとなるように振る舞いを定義します。

SelectOperationGeneratorTest
        every { SelenideUtil.selectListByCssSelector("$cssSelector > option") } returns optionList

optionListに含まれる2つのoptionに対して実際に処理されるモックの振る舞いを定義します。

SelectOperationGeneratorTest
        every { SelenideUtil.selectOptionByValueByCssSelector(cssSelector, value1) } returns Unit
        every { SelenideUtil.selectOptionByCssSelector(cssSelector, text1) } returns Unit
        every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, value1) } returns Unit
        every { SelenideUtil.selectOptionByValueByCssSelector(cssSelector, value2) } returns Unit
        every { SelenideUtil.selectOptionByCssSelector(cssSelector, text2) } returns Unit
        every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, value2) } returns Unit

テスト対象の操作を呼び出し、結果がselect-successResult.txtと一致することを検証します。

SelectOperationGeneratorTest
        var result = getTargetOperationGenerator().generateOperation(cssSelector)
        assertFileEquals("select-successResult.txt", result)

select-successResult.txtは以下のようになります。

select-successResult.txt
/**************** cssSelector cssSelectorValue の処理 start ****************/
//セレクトボックスの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")

//SELECTボックスを値で選択 value=<dummyTagName class disabled readonly href id name onclick onchange placeholder type value="value1">text1</dummyTagName>.value
SelenideUtil.selectOptionByValueByCssSelector("cssSelectorValue", "value1")

//SELECTボックスをオプションで選択 option=text1
SelenideUtil.selectOptionByCssSelector("cssSelectorValue", "text1")

//SELECTボックスの選択確認
SelenideUtil.shouldBeSelectedByCssSelector("cssSelectorValue > option[value='value1']")

//SELECTボックスを値で選択 value=<dummyTagName class disabled readonly href id name onclick onchange placeholder type value="value2">text2</dummyTagName>.value
SelenideUtil.selectOptionByValueByCssSelector("cssSelectorValue", "value2")

//SELECTボックスをオプションで選択 option=text2
SelenideUtil.selectOptionByCssSelector("cssSelectorValue", "text2")

//SELECTボックスの選択確認
SelenideUtil.shouldBeSelectedByCssSelector("cssSelectorValue > option[value='value2']")


最後に、SelenideUtilの各メソッドのverifyを実施します。

SelectOperationGeneratorTest
        for (testDataPair in listOf(Pair(value1, text1), Pair(value2, text2))) {
            verify(exactly = 1) {
                SelenideUtil.confirmExistenceByCssSelector(cssSelector)
                SelenideUtil.selectListByCssSelector("$cssSelector > option")
                SelenideUtil.selectOptionByValueByCssSelector(cssSelector, testDataPair.first)
                SelenideUtil.selectOptionByCssSelector(cssSelector, testDataPair.second)
                SelenideUtil.shouldBeValueByCssSelector(cssSelector, testDataPair.first)
            }
        }

結果をテンプレートに埋め込む処理

testClassNameはTestExampleOperationGenerator#generateの第2引数
previousActionStringListは、TestExampleOperationGenerator#generateの第3引数
testOperationListは、ボタン以外を対象として生成した操作処理のList<String>
testButtonOperationCollectionListは、ボタンを対象として生成した操作処理のList<String>
となります。

testOperationListtestButtonOperationCollectionListが分かれているのは、ボタンをクリックすると画面遷移する可能性があるので、
一つのボタンに対する操作処理は、一つのテストメソッドで生成する必要があるからです。

TemplateDataModelUtil
package jp.small_java_world.testopegen.util

class TemplateDataModelUtil {
    companion object {
        private const val SPACE_STRING = "        "

        fun generateDataModel(
            testClassName: String,
            previousActionStringList: List<String>,
            testOperationList: MutableList<String>,
            testButtonOperationCollectionList: MutableList<MutableList<String>>
        ): MutableMap<String, Any> {
            val dataModel = mutableMapOf<String, Any>()
            dataModel["testClassName"] = testClassName

            val previousActionBuilder = StringBuilder()
            previousActionStringList.forEach { previousActionBuilder.appendLine("$SPACE_STRING$it") }
            dataModel["previousAction"] = previousActionBuilder.substring(SPACE_STRING.length)

            val testMethodDataMapList = mutableListOf<Map<String, String>>()
            dataModel["testMethodDataList"] = testMethodDataMapList

            for ((index, testButtonOperations) in testButtonOperationCollectionList.withIndex()) {
                //各ボタン要素に対する操作をtestMethodDataMapListに追加
                val testMethodDataMap = generateTestMethodDataMap("button$index test operation", testButtonOperations)
                if (testMethodDataMap.isNotEmpty()) {
                    testMethodDataMapList.add(testMethodDataMap)
                }
            }

            //各ボタン以外の要素に対する操作をtestMethodDataMapListに追加
            val testMethodDataMap = generateTestMethodDataMap("input test operation", testOperationList)
            if (testMethodDataMap.isNotEmpty()) {
                testMethodDataMapList.add(testMethodDataMap)
            }

            return dataModel
        }

        private fun generateTestMethodDataMap(
            name: String,
            testButtonOperations: MutableList<String>
        ): Map<String, String> {
            val operationBuilder = StringBuilder()
            val testMethodDataMap = mutableMapOf<String, String>()
            testMethodDataMap["name"] = name

            for (testButtonOperation in testButtonOperations) {
                operationBuilder.appendLine("$SPACE_STRING$testButtonOperation")
            }

            if (operationBuilder.isNotEmpty()) {
                testMethodDataMap["operation"] = operationBuilder.toString().substring(SPACE_STRING.length)
            }
            return testMethodDataMap
        }
    }
}

結果をファイルに出力する処理

生成した操作処理などを引数で指定し、TemplateDataModelUtil#generateDataModelでテンプレートに埋め込む値を生成、結果ファイルに出力するとの処理になります。

TestOperationFileWriter
package jp.small_java_world.testopegen.util

import java.io.File
import java.nio.file.Files
import java.nio.file.Paths


class TestOperationFileWriter {
    companion object {
        private const val SPACE_STRING = "        "

        fun writeFile(
            classTemplateFileName: String,
            testClassName: String,
            previousActionStringList: List<String>,
            testOperationList: MutableList<String>,
            testButtonOperationCollectionList: MutableList<MutableList<String>>
        ) {
            val dateModel = TemplateDataModelUtil.generateDataModel(
                testClassName,
                previousActionStringList,
                testOperationList,
                testButtonOperationCollectionList
            )

            //操作を含んだファイルの出力先を確認し、存在しなければ作成
            val outputDirPath =
                Paths.get(".${File.separator}src${File.separator}test${File.separator}kotlin${File.separator}output")
            if (!Files.exists(outputDirPath)) {
                Files.createDirectory(outputDirPath)
            }

            //テンプレートの読み込み
            val template = TemplateFactory.create(classTemplateFileName)

            //ファイル出力
            File("${outputDirPath}${File.separator}$testClassName.kt").bufferedWriter()
                .use { bufferedWriter ->
                    template.process(dateModel, bufferedWriter)
                }
        }
    }

}

TestOperationFileWriterTest

package jp.small_java_world.testopegen.util

import freemarker.template.Template
import io.mockk.*
import jp.small_java_world.testopegen.TestBase
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.File
import java.io.Writer
import java.nio.file.Files
import java.nio.file.Paths

class TestOperationFileWriterTest : TestBase() {

    @BeforeEach
    fun beforeEach() {
        mockkObject(TemplateDataModelUtil)
        mockkObject(TemplateFactory)
        mockkStatic(Files::class)
    }

    override fun afterEach() = super.afterEach()

    @Test
    fun testWriteFile() {
        val classTemplateFileName = "classTemplateFileName"
        val testClassName = "testClassName"
        val previousActionStringList = listOf("previousActionStringListContent")
        val testOperationList = mutableListOf("testOperationListContent")
        val testButtonOperationCollectionList = mutableListOf(mutableListOf("testButtonOperationCollectionListContent"))

        val dateModel: MutableMap<String, Any> = mutableMapOf("dummyKey" to "dummyValue")

        every {
            TemplateDataModelUtil.generateDataModel(
                testClassName,
                previousActionStringList,
                testOperationList,
                testButtonOperationCollectionList
            )
        } returns dateModel

        val outputDirPath =
            Paths.get(".${File.separator}src${File.separator}test${File.separator}kotlin${File.separator}output")

        every {
            Files.exists(outputDirPath)
        } returns false

        every {
            Files.createDirectory(outputDirPath)
        } returns null

        val template = mockk<Template>()
        every {
            TemplateFactory.create(classTemplateFileName)
        } returns template

        every {
            template.process(dateModel, any<Writer>())
        } returns Unit

        TestOperationFileWriter.writeFile(
            classTemplateFileName,
            testClassName,
            previousActionStringList,
            testOperationList,
            testButtonOperationCollectionList
        )

        verify(exactly = 1) {
            TemplateDataModelUtil.generateDataModel(
                testClassName,
                previousActionStringList,
                testOperationList,
                testButtonOperationCollectionList
            )

            Files.exists(outputDirPath)
            Files.createDirectory(outputDirPath)
            TemplateFactory.create(classTemplateFileName)
            template.process(dateModel, any<Writer>())
        }

    }
}

各処理を呼び出す処理

必要な処理がそろったので各処理を呼び出して結果を生成します。

TestExampleOperationGenerator
package jp.small_java_world.testopegen

import jp.small_java_world.testopegen.analyzer.CssSelectorAnalyzer
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.generator.OperationGeneratorFactory
import jp.small_java_world.testopegen.util.TestOperationFileWriter


class TestExampleOperationGenerator() {
    var cssSelectorAnalyzer = CssSelectorAnalyzer()

    fun generate(
        classTemplateFileName: String,
        testClassName: String,
        previousActionStringList: List<String>,
        previousAction: () -> Unit
    ) {
        //inputタグとselectタグのorg.jsoup.select.Elementsを取得
        val targetElements = HtmlDocumentParser.getElements(previousAction)

        //ボタンのテスト操作の格納用 ボタン押すと画面遷移する可能性があるので、ボタンは1要素(HTMLの)につき1テストメソッドとするので別に管理
        var testButtonOperationCollectionList = mutableListOf<MutableList<String>>()

        //ボタン以外のテスト操作の格納用
        var testOperationList = mutableListOf<String>()

        //各タグを処理していく
        for (targetElement in targetElements) {

            // 現在の処理対象のinputTagElementに対応する Pair<String?, TargetElementType?>を取得
            var selectorElementTypePair = cssSelectorAnalyzer.getCssSelectorElementTypePair(targetElement)

            //TargetElementTypeに対応するOperationGeneratorを生成
            var operationGenerator = OperationGeneratorFactory.getOperationGenerator(selectorElementTypePair.second)

            if (operationGenerator != null) {
                when (selectorElementTypePair?.second) {
                    //TargetElementType.INPUT_BUTTONのときのみtestButtonOperationCollectionListに追加
                    TargetElementType.INPUT_BUTTON -> {
                        testButtonOperationCollectionList.add(
                            operationGenerator.generateOperation(
                                selectorElementTypePair.first
                            )
                        )
                        //ボタンを実際にクリックするので画面遷移していると後続の要素の処理が失敗するので前処理を呼び出し
                        previousAction.invoke()
                    }
                    else -> testOperationList.addAll(operationGenerator.generateOperation(selectorElementTypePair.first))
                }
            }
        }

        //ファイル出力
        TestOperationFileWriter.writeFile(
            classTemplateFileName,
            testClassName,
            previousActionStringList,
            testOperationList,
            testButtonOperationCollectionList
        )
    }
}

TestExampleOperationGeneratorTest

TestExampleOperationGeneratorのテストは以下のようになります。

TestExampleOperationGeneratorTest
package jp.small_java_world.testopegen

import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.verify
import jp.small_java_world.testopegen.analyzer.CssSelectorAnalyzer
import jp.small_java_world.testopegen.define.TargetElementType
import jp.small_java_world.testopegen.generator.ButtonOperationGenerator
import jp.small_java_world.testopegen.generator.OperationGeneratorFactory
import jp.small_java_world.testopegen.generator.TextOperationGenerator
import jp.small_java_world.testopegen.util.TestOperationFileWriter
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class TestExampleOperationGeneratorTest : TestBase() {
    var testExampleOperationGenerator = TestExampleOperationGenerator()
    lateinit var cssSelectorAnalyzer: CssSelectorAnalyzer

    @BeforeEach
    fun beforeEach() {
        mockkObject(HtmlDocumentParser)
        mockkObject(OperationGeneratorFactory)
        mockkObject(TestOperationFileWriter)

        cssSelectorAnalyzer = mockk<CssSelectorAnalyzer>()
        testExampleOperationGenerator.cssSelectorAnalyzer = cssSelectorAnalyzer
    }

    @AfterEach
    override fun afterEach() = super.afterEach()

    @Test
    fun testGenerate() {
        val buttonElement = Element("button")
        val textElement = Element("input")
        val getElementsResult = Elements(listOf(buttonElement, textElement))

        val previousAction = {}
        every { HtmlDocumentParser.getElements(previousAction) } returns getElementsResult

        val buttonPair = "buttonCss" to TargetElementType.INPUT_BUTTON
        val textPair = "textCss" to TargetElementType.INPUT_TEXT

        val buttonOperationGenerator = mockk<ButtonOperationGenerator>()
        val textOperationGenerator = mockk<TextOperationGenerator>()

        every { cssSelectorAnalyzer.getCssSelectorElementTypePair(buttonElement) } returns buttonPair
        every { cssSelectorAnalyzer.getCssSelectorElementTypePair(textElement) } returns textPair

        every { OperationGeneratorFactory.getOperationGenerator(buttonPair.second) } returns buttonOperationGenerator
        every { OperationGeneratorFactory.getOperationGenerator(textPair.second) } returns textOperationGenerator

        val buttonGenerateOperationResult = mutableListOf("buttonResult")
        val textGenerateOperationResult = mutableListOf("textResult")
        every { buttonOperationGenerator.generateOperation(buttonPair.first) } returns buttonGenerateOperationResult
        every { textOperationGenerator.generateOperation(textPair.first) } returns textGenerateOperationResult

        val classTemplateFileName = "classTemplateFileName"
        val testClassName = "testClassName"
        val previousActionStringList = listOf("previousActionStringListContent")

        every {
            TestOperationFileWriter.writeFile(
                classTemplateFileName,
                testClassName,
                previousActionStringList,
                textGenerateOperationResult,
                mutableListOf(buttonGenerateOperationResult)
            )
        } returns Unit

        testExampleOperationGenerator.generate(
            classTemplateFileName,
            testClassName,
            previousActionStringList,
            previousAction
        )

        verify(exactly = 1) {
            HtmlDocumentParser.getElements(previousAction)
            cssSelectorAnalyzer.getCssSelectorElementTypePair(buttonElement)
            cssSelectorAnalyzer.getCssSelectorElementTypePair(textElement)
            OperationGeneratorFactory.getOperationGenerator(buttonPair.second)
            OperationGeneratorFactory.getOperationGenerator(textPair.second)
            buttonOperationGenerator.generateOperation(buttonPair.first)
            textOperationGenerator.generateOperation(textPair.first)

            TestOperationFileWriter.writeFile(
                classTemplateFileName,
                testClassName,
                previousActionStringList,
                textGenerateOperationResult,
                mutableListOf(buttonGenerateOperationResult)
            )
        }
    }

}

生成される操作処理の例

対象のHTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>操作対象サンプル</title>
</head>
<body>
  <!-- notDisplayButtonを3秒後に非表示化 -->
  <script>
  function setDisplayNo(){
    let button = document.getElementById("notDisplayButton");
    if (button){ button.style.display ="none";}
  }

  window.addEventListener('load', function(){
    window.setTimeout(function(){
        setDisplayNo();
    }, 3000);
  });
  </script>

  <h1>操作対象 input サンプル</h1>

  <label>button:</label>
  <input type="button" id="addButton" value="追加ボタン">
  <input type="button" id="deleteButton" value="削除ボタン">
  <input type="button" id="delayDeleteButton" value="遅延削除ボタン">
  <input type="button" id="notDisplayButton" name="notDisplayButton" value="非表示のボタン">
  <div style="visibility:hidden">
    <input type="button" id="blockedButton" name="blockedButton" value="親要素が非表示のボタン">
  </div>
  <HR>

  <label>radio:</label>
  <input type="radio" name="grade" id="grade1" value="1"><label for="grade1">1年生</label>
  <input type="radio" name="grade" id="grade2" value="2"><label for="grade2">2年生</label>
  <HR>

  <label>checkbox:</label>
  <input type="checkbox" name="lang" id="lang1" value="1"><label for="lang1">Java</label>
  <input type="checkbox" name="lang" id="lang2" value="2"><label for="lang2">C#</label>
  <HR>

  <label>text:</label>

  <div id="p-input1-2-1">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>
  <div id="p-input1-2-2">
     <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  </div>

  <BR>
  <input type="text" name="input1-1" id="input1" maxlength="10" value="input1-1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-2" hoge="1">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="2">
  <input type="text" name="input1-2" id="input1" maxlength="10" value="input1-3" hoge="3">
  <BR>


</body>
</html>

生成されたクラス

package output

import com.codeborne.selenide.Configuration
import com.codeborne.selenide.Selenide
import com.codeborne.selenide.WebDriverRunner
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import jp.small_java_world.testopegen.util.*

class InputTest {
    companion object {
        @JvmStatic
        @BeforeAll
        fun beforeAll() {
            Configuration.browser = WebDriverRunner.CHROME;
        }
    }

    @BeforeEach
    fun beforeEach() {
        Selenide.open("file://C:/workspace/SelenideOperationGenerator/html/input.html")
        SelenideUtil.shouldHaveAttributeByCssSelector("#notDisplayButton", "style", "display: none;")

    }

    @Test
    fun `button0 test operation`() {
        /**************** cssSelector #addButton の処理 start ****************/
        //ボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#addButton")

        SelenideUtil.clickByCssSelector("#addButton")


    }

    @Test
    fun `button1 test operation`() {
        /**************** cssSelector #deleteButton の処理 start ****************/
        //ボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#deleteButton")

        SelenideUtil.clickByCssSelector("#deleteButton")


    }

    @Test
    fun `button2 test operation`() {
        /**************** cssSelector #delayDeleteButton の処理 start ****************/
        //ボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#delayDeleteButton")

        SelenideUtil.clickByCssSelector("#delayDeleteButton")


    }

    @Test
    fun `button3 test operation`() {
        /**************** cssSelector #notDisplayButton の処理 start ****************/
        //ボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#notDisplayButton")

        // clickByCssSelector fail
        //SelenideUtil.clickByCssSelector("#notDisplayButton")

        SelenideUtil.clickByCssSelectorUseJS("#notDisplayButton")


    }

    @Test
    fun `button4 test operation`() {
        /**************** cssSelector #blockedButton の処理 start ****************/
        //ボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#blockedButton")

        // clickByCssSelector fail
        //SelenideUtil.clickByCssSelector("#blockedButton")

        SelenideUtil.clickByCssSelectorUseJS("#blockedButton")


    }

    @Test
    fun `input test operation`() {
        /**************** cssSelector #grade1 の処理 start ****************/
        //ラジオボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#grade1")

        //ラジオボタンの選択
        SelenideUtil.selectRadioByCssSelector("input[name=grade]", "1")

        //ラジオボタンの選択の検証
        SelenideUtil.shouldBeSelectedByCssSelector("#grade1")

        /**************** cssSelector #grade2 の処理 start ****************/
        //ラジオボタンの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#grade2")

        //ラジオボタンの選択
        SelenideUtil.selectRadioByCssSelector("input[name=grade]", "2")

        //ラジオボタンの選択の検証
        SelenideUtil.shouldBeSelectedByCssSelector("#grade2")

        /**************** cssSelector #lang1 の処理 start ****************/
        //チェックボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#lang1")

        //チェックボックスのチェック
        SelenideUtil.checkByCssSelector("#lang1")

        //チェックされたことの検証
        SelenideUtil.shouldBeSelectedByCssSelector("#lang1")

        //チェックボックスのアンチェック
        SelenideUtil.unCheckByCssSelector("#lang1")

        //チェックさていないことの検証
        SelenideUtil.shouldBeNotSelectedByCssSelector("#lang1")

        /**************** cssSelector #lang2 の処理 start ****************/
        //チェックボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#lang2")

        //チェックボックスのチェック
        SelenideUtil.checkByCssSelector("#lang2")

        //チェックされたことの検証
        SelenideUtil.shouldBeSelectedByCssSelector("#lang2")

        //チェックボックスのアンチェック
        SelenideUtil.unCheckByCssSelector("#lang2")

        //チェックさていないことの検証
        SelenideUtil.shouldBeNotSelectedByCssSelector("#lang2")

        /**************** cssSelector #p-input1-2-1 > #input1 の処理 start ****************/
        //テキストボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#p-input1-2-1 > #input1")

        //テキストボックスへの入力
        SelenideUtil.inputTextByCssSelector("#p-input1-2-1 > #input1", "kZtp")

        //テキストボックスへ入力した値の検証
        SelenideUtil.shouldBeValueByCssSelector("#p-input1-2-1 > #input1", "kZtp")

        /**************** cssSelector #p-input1-2-2 > #input1 の処理 start ****************/
        //テキストボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("#p-input1-2-2 > #input1")

        //テキストボックスへの入力
        SelenideUtil.inputTextByCssSelector("#p-input1-2-2 > #input1", "SiS7")

        //テキストボックスへ入力した値の検証
        SelenideUtil.shouldBeValueByCssSelector("#p-input1-2-2 > #input1", "SiS7")

        /**************** cssSelector input[name='input1-1'] の処理 start ****************/
        //テキストボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("input[name='input1-1']")

        //テキストボックスへの入力
        SelenideUtil.inputTextByCssSelector("input[name='input1-1']", "TuOC")

        //テキストボックスへ入力した値の検証
        SelenideUtil.shouldBeValueByCssSelector("input[name='input1-1']", "TuOC")

        /**************** cssSelector input[hoge='1'] の処理 start ****************/
        //テキストボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("input[hoge='1']")

        //テキストボックスへの入力
        SelenideUtil.inputTextByCssSelector("input[hoge='1']", "KPWe")

        //テキストボックスへ入力した値の検証
        SelenideUtil.shouldBeValueByCssSelector("input[hoge='1']", "KPWe")

        /**************** cssSelector input[hoge='2'] の処理 start ****************/
        //テキストボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("input[hoge='2']")

        //テキストボックスへの入力
        SelenideUtil.inputTextByCssSelector("input[hoge='2']", "l8yz")

        //テキストボックスへ入力した値の検証
        SelenideUtil.shouldBeValueByCssSelector("input[hoge='2']", "l8yz")

        /**************** cssSelector html > body > input[hoge='3'] の処理 start ****************/
        //テキストボックスの存在確認
        SelenideUtil.confirmExistenceByCssSelector("html > body > input[hoge='3']")

        //テキストボックスへの入力
        SelenideUtil.inputTextByCssSelector("html > body > input[hoge='3']", "iKJO")

        //テキストボックスへ入力した値の検証
        SelenideUtil.shouldBeValueByCssSelector("html > body > input[hoge='3']", "iKJO")


    }

}

生成時に利用したmainメソッド

package jp.small_java_world.testopegen

import com.codeborne.selenide.Selenide
import jp.small_java_world.testopegen.define.CommonDef.Companion.PROJECT_ROOT_PATH
import jp.small_java_world.testopegen.util.SelenideUtil

fun main(args: Array<String>) {
    val testExampleOperationGenerator = TestExampleOperationGenerator()
    val targetHtmlFullPath = "file://$PROJECT_ROOT_PATH/html/input.html"

    testExampleOperationGenerator.generate(
        "testOperationClassTemplate.ftl",
        "InputTest",
        listOf(
            "Selenide.open(\"$targetHtmlFullPath\")",
            "SelenideUtil.shouldHaveAttributeByCssSelector(\"#notDisplayButton\", \"style\", \"display: none;\")"
        )
    )
    {
        Selenide.open(targetHtmlFullPath)
        SelenideUtil.shouldHaveAttributeByCssSelector("#notDisplayButton", "style", "display: none;")
    }
}

完成版の全ソースは
GitHubリポジトリ
に登録しております。

5
9
0

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
5
9