#はじめに
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
となってしまいます。
「Selenium IDE」は「Chrome Developer Tools」に比べると賢いです。結果は以下のようになります。
全部正しいですね・・・、残念・・・
まあ「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
}
}
usingJavaScript
がtrue
の場合は以下の操作が生成されます。
@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)
}
usingJavaScript
がfalse
の場合は以下の操作が生成されます。
@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ファイルが作成されます。
#実行方法
TestExampleOperationGenerator#generateを呼び出して実行します。
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と判定優先順位は以下のようになります。
#input1
input[name='input1-2']
input[hoge='3']
#p-input1-2-1 > #input1
#p-input1-2-1 > input[name='input1-2']
#p-input1-2-1 > input[hoge='3']
上位からSelenideUtil#isDuplicateByCssSelector
で一意判定を行い、一意の場合は一意識別子として認定します。
結果として、#p-input1-2-1 > #input1
が一意識別子となります。
@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
}
}
}
tagName
とinputType
からelementType:TargetElementType
を決定しています。
getElementType
は、面白くないですね・・・
val tagName = targetElement.tagName()
val inputType = targetElement.attr("type")
val elementType = getElementType(tagName, inputType)
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
}
}
ここから一意識別子の判定となります。
var cssSelectorValue = targetElement.cssSelector()
//targetElement.cssSelector()で要素が重複していない場合は、targetElement.cssSelector()の値を返却
if (!SelenideUtil.isDuplicateByCssSelector(cssSelectorValue!!)) {
return cssSelectorValue to elementType
}
org.jsoup.nodes.Element#cssSelectorの結果が一意識別子であるか判定しています。
// 親要素のCssSelectorを取得
val parentCssSelectorValue = getParentCssSelector(targetElement)
親要素のCssSelectorを取得しています。
getParentCssSelectorですが、要素のorg.jsoup.nodes.Element#cssSelectorの結果が一意識別子であれば、その値を適用、そうでなればもう一段上がって、との処理となります。
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
}
ここから本番です。
// 評価する属性のリストの作成のために全属性のリストを取得
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
で属性の一覧を取得し、id
とname
の属性を取り出したのちに、attributesから削除します。
id
とname
は優先して判定に利用したいので、後で先頭に持ってきます。
//idとname属性を優先するattributeListを生成
val attributeList = listOf(idAttribute, nameAttribute) + attributes
id
とname
は優先して判定に利用したいので、新たなリストを生成します。
準備は整ったので、あとはループして判定するだけです。
// まずは、対象の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を解析して結果を検証するようになります。
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
@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)
}
<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
@Test
fun `test getCssSelectorElementTypePair input radio`() {
val expectedResultList =
listOf(
"#grade1" to TargetElementType.INPUT_RADIO,
"#grade2" to TargetElementType.INPUT_RADIO
)
testGetCssSelectorElementTypePairCommon("input_radio.html", expectedResultList)
}
<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
@Test
fun `test getCssSelectorElementTypePair input checkbox`() {
val expectedResultList =
listOf(
"#lang1" to TargetElementType.INPUT_CHECKBOX,
"#lang2" to TargetElementType.INPUT_CHECKBOX,
)
testGetCssSelectorElementTypePairCommon("input_checkbox.html", expectedResultList)
}
<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
@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)
}
<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を各実装クラスが実装することで処理を実現します。
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!!)
で実際の存在確認を行います。
@JvmStatic
fun confirmExistenceByCssSelector(cssSelector: String): Boolean {
return try {
Selenide.`$$`(By.cssSelector(cssSelector)).shouldHaveSize(1)
true;
} catch (e: Throwable) {
false;
}
}
存在確認が成功すれば、確認処理の文字列を生成し、testOperationListに追加します。
val confirmOperation =
CommonDef.CONFIRM_EXISTENCE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
testOperationList.add("//${elementName}の存在確認")
testOperationList.add(confirmOperation)
testOperationList.add("")
const val TARGET_CSS_SELECTOR = "%targetCssSelector"
const val CONFIRM_EXISTENCE_TEMPLATE = "SelenideUtil.confirmExistenceByCssSelector(\"$TARGET_CSS_SELECTOR\")"
ここまでが共通処理となります。
###OperationGeneratorのテスト
####OperationGeneratorインターフェースの実装クラスのテスト
OperationGeneratorインターフェースの実装クラスのテストクラスは以下のような構成となります。
####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!!)が失敗する場合のテストのみ実装します。
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) }
}
}
@BeforeEach
override fun beforeEach() {
super.beforeEach()
}
OperationGeneratorTestBase
のopen fun beforeEach() = mockkObject(SelenideUtil)
を呼び出しSelenideUtilをモック化しています。
@AfterEach
override fun afterEach() {
super.afterEach()
}
TestBase
のopen fun afterEach() = unmockkAll()
を呼び出しSelenideUtilのモック化を解除しています。
override fun getTargetOperationGenerator(): OperationGenerator {
return TextOperationGenerator()
}
テスト対象はTextOperationGeneratorですので、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) }
}
モック化されたSelenideUtil
のconfirmExistenceByCssSelector(cssSelector)
の結果をfalseに固定し、
getTargetOperationGenerator().generateOperation(cssSelector)
の結果が期待値ファイルの中身と一致するか検証しています。
/**************** cssSelector cssSelectorValue の処理 start ****************/
//confirmExistenceByCssSelector fail
###TextOperationGenerator
input textに対応した実装クラスとなります。
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)
で実際に生成した文字列を入力しています。
@JvmStatic
fun inputTextByCssSelector(cssSelector: String, text: String) {
selectByCssSelector(cssSelector).value = text
}
生成した文字列を入力する操作をtestOperationListに追加しています。
val inputOperation = CommonDef.INPUT_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
.replace(CommonDef.INPUT_VALUE, inputValue)
testOperationList.add(inputOperation)
testOperationList.add("")
const val TARGET_CSS_SELECTOR = "%targetCssSelector"
const val INPUT_VALUE = "%inputTextValue"
const val INPUT_VALUE_TEMPLATE =
"SelenideUtil.inputTextByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"
入力した文字列が入力されていることを検証しています。
SelenideUtil.shouldBeValueByCssSelector(cssSelector, inputValue)
@JvmStatic
fun shouldBeValueByCssSelector(cssSelector: String, expect: String) {
selectByCssSelector(cssSelector).shouldBe(value(expect))
}
入力した文字列が入力されていることを検証する操作をtestOperationListに追加しています。
val confirmOperation = CommonDef.CONFIRM_VALUE_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
.replace(CommonDef.INPUT_VALUE, inputValue)
testOperationList.add(confirmOperation)
testOperationList.add("")
const val CONFIRM_VALUE_TEMPLATE =
"SelenideUtil.shouldBeValueByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"
generateRandomLetterOrDigit
メソッドは以下の通りです。
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)
}
}
}
every { SelenideUtil.confirmExistenceByCssSelector(cssSelector) } returns true
SelenideUtil.confirmExistenceByCssSelector(cssSelector)
が失敗するテストケースは、OperationGeneratorTest
で実装済みですので、
SelenideUtil.confirmExistenceByCssSelector(cssSelector)
の結果がtrueになるとの振る舞いを定義しています。
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は以下のようになります。
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)
}
every { SelenideUtil.inputTextByCssSelector(cssSelector, dummyStr) } returns Unit
every { SelenideUtil.shouldBeValueByCssSelector(cssSelector, dummyStr) } returns Unit
SelenideUtil.inputTextByCssSelector
とSelenideUtil.shouldBeValueByCssSelector
が呼び出されたときの振る舞いを定義しています。
モックの準備が整ったので、OperationGenerator#generateOperation
を呼び出し、生成された操作処理のList<String>が期待値ファイルの中身と一致することを検証します。
var result = getTargetOperationGenerator().generateOperation(cssSelector)
assertFileEquals("text-successResult.txt", result)
/**************** cssSelector cssSelectorValue の処理 start ****************/
//テキストボックスの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")
//テキストボックスへの入力
SelenideUtil.inputTextByCssSelector("cssSelectorValue", "dummy")
//テキストボックスへ入力した値の検証
SelenideUtil.shouldBeValueByCssSelector("cssSelectorValue", "dummy")
最後にモックのverifyを実施します。
verify(exactly = 1) {
SelenideUtil.confirmExistenceByCssSelector(cssSelector)
generateRandomLetterOrDigit(any<Int>())
SelenideUtil.inputTextByCssSelector(cssSelector, dummyStr)
SelenideUtil.shouldBeValueByCssSelector(cssSelector, dummyStr)
}
###ButtonOperationGenerator
input buttonの操作処理を生成するクラスとなります。
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でクリックするようになります。
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
の場合は通常クリック処理はコメントとして生成します。
val clickOperation = CommonDef.CLICK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector!!)
testOperationList.add(if (usingJavaScript) "//$clickOperation" else clickOperation)
testOperationList.add("")
const val CLICK_TEMPLATE = "SelenideUtil.clickByCssSelector(\"$TARGET_CSS_SELECTOR\")"
繰り返しになりますが、usingJavaScript=true
の場合はJavaScriptでクリックする操作を生成します。
if (usingJavaScript) {
val clickUseJsOperation =
CommonDef.CLICK_USE_JS_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
testOperationList.add(clickUseJsOperation)
testOperationList.add("")
}
const val CLICK_USE_JS_TEMPLATE = "SelenideUtil.clickByCssSelectorUseJS(\"$TARGET_CSS_SELECTOR\")"
@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のテスト
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に含んでいます。
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でモックの振る舞いを分岐して、操作処理の生成結果を期待値のファイルと比較します。
@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
)
}
}
期待値のファイルは以下の通りです。
/**************** cssSelector cssSelectorValue の処理 start ****************/
//ボタンの存在確認
SelenideUtil.confirmExistenceByCssSelector("cssSelectorValue")
// clickByCssSelector fail
//SelenideUtil.clickByCssSelector("cssSelectorValue")
SelenideUtil.clickByCssSelectorUseJS("cssSelectorValue")
/**************** 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;
}
}
チェックボックスのチェック操作を生成します。
val checkOperation = CommonDef.CHECK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
testOperationList.add(checkOperation)
testOperationList.add("")
const val CHECK_TEMPLATE = "SelenideUtil.checkByCssSelector(\"$TARGET_CSS_SELECTOR\")"
@JvmStatic
fun checkByCssSelector(cssSelector: String) {
selectByCssSelector(cssSelector).isSelected = true
}
チェックボックスがチェックされていることの検証操作を生成します。
val confirmOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
testOperationList.add(confirmOperation)
testOperationList.add("")
const val CONFIRM_SELECTED_TEMPLATE = "SelenideUtil.shouldBeSelectedByCssSelector(\"$TARGET_CSS_SELECTOR\")"
@JvmStatic
fun shouldBeSelectedByCssSelector(cssSelector: String) {
selectByCssSelector(cssSelector).shouldBe(selected)
}
チェックボックスのチェックをはずす操作を生成します。
val unCheckOperation = CommonDef.UNCHECK_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
testOperationList.add(unCheckOperation)
testOperationList.add("")
const val UNCHECK_TEMPLATE = "SelenideUtil.unCheckByCssSelector(\"$TARGET_CSS_SELECTOR\")"
@JvmStatic
fun unCheckByCssSelector(cssSelector: String) {
selectByCssSelector(cssSelector).isSelected = false
}
チェックボックスがチェックされていないことの検証操作を生成します。
val confirmUnCheckOperation =
CommonDef.CONFIRM_NOT_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
testOperationList.add(confirmUnCheckOperation)
testOperationList.add("")
const val CONFIRM_NOT_SELECTED_TEMPLATE =
"SelenideUtil.shouldBeNotSelectedByCssSelector(\"$TARGET_CSS_SELECTOR\")"
@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の属性値を取得します。
var radioName = SelenideUtil.getNameByCssSelector(cssSelector!!)
var radioValue = SelenideUtil.getValueByCssSelector(cssSelector)
@JvmStatic
fun getValueByCssSelector(cssSelector: String): String? {
return selectByCssSelector(cssSelector).value
}
@JvmStatic
fun getNameByCssSelector(cssSelector: String): String? {
return selectByCssSelector(cssSelector).getAttribute("name")
}
次に、実際にラジオボタンを選択します。
SelenideUtil.selectRadioByCssSelector("input[name=$radioName]", radioValue!!)
@JvmStatic
fun selectRadioByCssSelector(cssSelector: String, value: String) {
selectByCssSelector(cssSelector).selectRadio(value)
}
ラジオボタンの選択操作を生成します。
var selectOperation =
CommonDef.SELECT_RADIO_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, "input[name=$radioName]")
.replace(CommonDef.INPUT_VALUE, radioValue)
testOperationList.add(selectOperation)
testOperationList.add("")
const val SELECT_RADIO_TEMPLATE =
"SelenideUtil.selectRadioByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"
実際にラジオボタンが選択されていることを検証します。
SelenideUtil.shouldBeSelectedByCssSelector(cssSelector)
@JvmStatic
fun shouldBeSelectedByCssSelector(cssSelector: String) {
selectByCssSelector(cssSelector).shouldBe(selected)
}
ラジオボタンが選択されていることを検証のを行う操作を生成します。
val confirmOperation = CommonDef.CONFIRM_SELECTED_TEMPLATE.replace(CommonDef.TARGET_CSS_SELECTOR, cssSelector)
testOperationList.add(confirmOperation)
testOperationList.add("")
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は以下のようになります。
/**************** 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ボックスの選択確認
となります。
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を取得します。
val options = SelenideUtil.selectListByCssSelector("$cssSelector > option")
以降は、optionsの各要素に対する処理となります。
for (option in options) {
まずは、実際にセレクトボックスを値で選択します。
SelenideUtil.selectOptionByValueByCssSelector(cssSelector!!, option.value!!)
@JvmStatic
fun selectOptionByValueByCssSelector(cssSelector: String, value: String) {
selectByCssSelector(cssSelector).selectOptionByValue(value)
}
次に実際にセレクトボックスをオプション(text)で選択します。
SelenideUtil.selectOptionByCssSelector(cssSelector!!, option.text())
@JvmStatic
fun selectOptionByCssSelector(cssSelector: String, value: String) {
selectByCssSelector(cssSelector).selectOption(value)
}
引き続き、実際にセレクトボックスの選択確認を行います。
SelenideUtil.shouldBeValueByCssSelector(cssSelector!!, option.text())
@JvmStatic
fun shouldBeValueByCssSelector(cssSelector: String, expect: String) {
selectByCssSelector(cssSelector).shouldBe(value(expect))
}
これ以降は、前述の実際の操作に対応する操作処理の生成となります。
セレクトボックスを値で選択する操作の生成
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("")
const val SELECT_OPTION_BY_VALUE_TEMPLATE =
"SelenideUtil.selectOptionByValueByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"
セレクトボックスをオプション(text)で選択する操作の生成
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("")
const val SELECT_SELECT_OPTION_TEMPLATE =
"SelenideUtil.selectOptionByCssSelector(\"$TARGET_CSS_SELECTOR\", \"$INPUT_VALUE\")"
セレクトボックスの選択確認の操作の生成
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("")
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となるように振る舞いを定義します。
every { SelenideUtil.selectListByCssSelector("$cssSelector > option") } returns optionList
optionListに含まれる2つのoptionに対して実際に処理されるモックの振る舞いを定義します。
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と一致することを検証します。
var result = getTargetOperationGenerator().generateOperation(cssSelector)
assertFileEquals("select-successResult.txt", result)
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を実施します。
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>
となります。
testOperationList
とtestButtonOperationCollectionList
が分かれているのは、ボタンをクリックすると画面遷移する可能性があるので、
一つのボタンに対する操作処理は、一つのテストメソッドで生成する必要があるからです。
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でテンプレートに埋め込む値を生成、結果ファイルに出力するとの処理になります。
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>())
}
}
}
##各処理を呼び出す処理
必要な処理がそろったので各処理を呼び出して結果を生成します。
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のテストは以下のようになります。
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リポジトリ
に登録しております。