yfj2’s Automatic Web Test Related Blog

yfj2のWEBテスト自動化に関わるブログ

【入門】Geb+SpockではじめるWebテスト~リファクタリング編~

【入門】Geb+SpockではじめるWebテスト~リファクタリング編~
著者:ふじさわゆうき

この記事は、以下の記事の続きです。
初めて訪問した方は以下の記事を参照してください。

目次

  1. 今回の目的
  2. GoogleWikipediaTestの問題点
  3. 問題解決とリファクタリング(1)
  4. 問題解決とリファクタリング(2)
  5. 問題解決とリファクタリング(3)
  6. まとめ

1. 今回の目的

Geb本家で示されているテスト例のもとに作成した"GoogleWikipediaTest"の問題点を洗い出してリファクタリングすること
本家:http://www.gebish.org/manual/current/intro.html#testing

2. GoogleWikipediaTestの問題点

GoogleWikipediaTestの問題点を分析してみたところ、以下問題点があった。

  1. テストケース"GoogleWikipediaMainTest.groovy"がModuleなどの内部実装を意識しなければならない作りになっている
    • 「search.field.value("wikipedia")」は、「search("wikipedia")」にすれば内部実装を意識しなくてよくなる
    • 「firstResultLink.click()」は、「clickFirstResultLink」にすれば内部実装を意識しなくてよくなる
    • など
  2. GoogleSearchModuleのbuttonの指定が間違っている。サンプルでは実行していないのでエラーが発生していないだけ。

以下、現状のgoogle検索ボタンのHTML。inputタグは使用していない。

<button class="gbqfb" aria-label="Google Search" name="btnG" id="gbqfb">
	<span class="gbqfi gb_Sa"></span>
</button>

3. 部分一致のキーワード検索テストができない
以下のようなコードにするとエラーが発生する

class GoogleWikipediaMainTest extends GebSpec {

	def "first result for wikipedia search should be wikipedia"() {
		given:
		to GoogleHomePage

		expect:
		at GoogleHomePage

		when:
		search.field.value("test")

		then:
		waitFor { at GoogleResultsPage }

		when:
		search.field.value("wikipedia")
		
		then:
		waitFor { at GoogleResultsPage }

		and:
		firstResultLink.text() == "Wikipedia"

		when:
		firstResultLink.click()

		then:
		waitFor { at WikipediaPage }
	}
}

3. 問題解決とリファクタリング(1)

問題

  • テストケース"GoogleWikipediaMainTest.groovy"がModuleなどの内部実装を意識しなければならない作りになっている
    • 「search.field.value("wikipedia")」は、「search("wikipedia")」にすれば内部実装を意識しなくてよくなる
    • 「firstResultLink.click()」は、「clickFirstResultLink」にすれば内部実装を意識しなくてよくなる

リファクタ手順

  1. GoogleHomePageに"search"メソッドを追加する
  2. GoogleWikipediaMainTestの"search.field.value"を"search"に修正する
  3. GoogleWikipediaMainTestを実行して動作確認
  4. GoogleResultsPageに"clickFirstResultLink"メソッドを追加する
  5. GoogleWikipediaMainTestの"firstResultLink.click"を"firstResultLinkClick()"に修正する
  6. GoogleWikipediaMainTestを実行して動作確認

実装

  • "search"メソッドを追加する
class GoogleHomePage extends Page {

	// pages can define their location, either absolutely or relative to a base
	static url = "http://google.com/ncr"

	// “at checkers” allow verifying that the browser is at the expected page
	static at = { title == "Google" }

	static content = {
		// include the previously defined module
		search { module GoogleSearchModule, buttonValue: "Google Search" }
	}
        
        //"search"メソッドを追加
	public void search(String keyword){
		search.field.value(keyword)
	}
}
  • "search.field.value"を"search"に修正
class GoogleWikipediaMainTest extends GebSpec {

	def "first result for wikipedia search should be wikipedia"() {
		given:
		to GoogleHomePage

		expect:
		at GoogleHomePage

		when:
                //"search.field.value"を"search"に修正
		search("wikipedia")

		then:
		waitFor { at GoogleResultsPage }

		and:
		firstResultLink.text() == "Wikipedia"

		when:
		firstResultLink.click()

		then:
		waitFor { at WikipediaPage }
	}
}
  • "clickFirstResultLink"メソッドを追加する
class GoogleResultsPage extends Page {
	static at = { title.endsWith "Google Search" }
	static content = {
		// reuse our previously defined module
		search { module GoogleSearchModule, buttonValue: "Search" }

		// content definitions can compose and build from other definitions
		results { $("div.g") }
		result { i -> results[i] }
		resultLink { i -> result(i).find("a") }
		firstResultLink { resultLink(0) }
	}
	//firstResultLinkClickを追加
	public void firstResultLinkClick(){
		firstResultLink.click()
	}
}
  • "firstResultLink.click()"を"firstResultLinkClick()"に修正する
class GoogleWikipediaMainTest extends GebSpec {

	def "first result for wikipedia search should be wikipedia"() {
		given:
		to GoogleHomePage

		expect:
		at GoogleHomePage

		when:
                //"search.field.value"を"search"に修正
		search("wikipedia")

		then:
		waitFor { at GoogleResultsPage }

		and:
		firstResultLink.text() == "Wikipedia"

		when:
                //"firstResultLink.click()"を"firstResultLinkClick()"に修正
		firstResultLinkClick()

		then:
		waitFor { at WikipediaPage }
	}
}

4. 問題解決とリファクタリング(2)

問題

  • GoogleSearchModuleのbuttonの指定が間違っている。サンプルでは実行していないのでエラーが発生していないだけ。以下、現状のgoogle検索ボタンのHTML。inputタグは使用していない。
<button class="gbqfb" aria-label="Google Search" name="btnG" id="gbqfb">
	<span class="gbqfi gb_Sa"></span>
</button>

リファクタ手順

  1. GoogleSearchModuleの"buttonValue"を"buttonName"に変更する
  2. GoogleSearchModuleの"$("input", value: buttonValue)"を$("button", name: buttonName)"に変更する
    • inputタグがbuttonタグに変更になっていたので修正する
    • また、valueだとデザインの変更を受けやすいので、nameに変更する。nameの場合は、httpリクエストに利用されるため、変更の可能される可能性が低い
  3. GoogleSearchModuleの変更に対して、GoogleResultsPageとGoogleHomePageを修正する
  4. GoogleWikipediaMainTestを実行して動作確認

実装

  • "$("input", value: buttonValue)"を$("button", name: buttonName)"に変更する
class GoogleSearchModule extends Module {

	// a parameterised value set when the module is included
	def buttonName

	// the content DSL
	static content = {

		// name the search input control “field”, defining it with the jQuery like navigator
		field { $("input", name: "q") }

		// the search button declares that it takes us to the results page, and uses the
		// parameterised buttonName to define itself
		button(to: GoogleResultsPage) {
			$("button", name: buttonName)
		}
	}
}
  • GoogleSearchModuleの変更に対して、GoogleResultsPageとGoogleHomePageを修正する
    • search{ module GoogleSearchModule, buttonName: "btnG" }
class GoogleHomePage extends Page {

	// pages can define their location, either absolutely or relative to a base
	static url = "http://google.com/ncr"

	// “at checkers” allow verifying that the browser is at the expected page
	static at = { title == "Google" }

	static content = {
		// include the previously defined module
		search{ module GoogleSearchModule, buttonName: "btnG" }
	}

	public void search(String keyword){
		search.field.value(keyword)
	}
}


class GoogleResultsPage extends Page {
	static at = { title.endsWith "Google Search" }
	static content = {
		// reuse our previously defined module
		search{ module GoogleSearchModule, buttonName: "btnG" }

		// content definitions can compose and build from other definitions
		results { $("div.g") }
		result { i -> results[i] }
		resultLink { i -> result(i).find("a") }
		firstResultLink { resultLink(0) }
	}
	//firstResultLinkClickを追加
	public void firstResultLinkClick(){
		firstResultLink.click()
	}
}

5. 問題解決とリファクタリング(3)

問題

  • 部分一致のキーワード検索テストができない
    • 以下のようなコードにするとエラーが発生する
    • 現状のコードだと、完全一致のajax動作を前提としていることが原因
    • ここで先ほどのGoogleSearchModuleのbuttonの指定リファクタリングが生きてくる
    • 要は、検索と同時に検索ボタンをクリックしてやればよい
class GoogleWikipediaMainTest extends GebSpec {
	def "first result for wikipedia search should be wikipedia"() {
		given:
		to GoogleHomePage

		expect:
		at GoogleHomePage

		when:
		search("test")

		then:
		waitFor { at GoogleResultsPage }

		when:
		search("wikipedia")

		then:
		waitFor { at GoogleResultsPage }

		and:
		firstResultLink.text() == "Wikipedia"

		when:
		firstResultLinkClick()

		then:
		waitFor { at WikipediaPage }
	}
}

リファクタ手順

  1. GoogleResultsPageに"public void search(String keyword)"を追加する
  2. GoogleHomePage,GoogleResultsPageの"public void search(String keyword)"に"search.button.click()"を追加する
    • "wikipedia"で検索した場合は、検索結果に合致したページがあったためにajaxにより検索結果が表示された。しかし、"test"の場合は、合致したページがajaxによる検索結果が表示されないために検索ボタンを明示的に押してやる必要がある。("test"でajax結果が出た場合は、ブラウザキャッシュが残っているはずなのでクリアしてから再度、試してみてください)
  3. "waitFor { firstResultLink.text() == "Wikipedia"}"と"waitFor"を追記する
    • waitForがないと、firstResultLinkが表示される前に判定してしまい、エラーになってしまうのでそれを防ぐ
  • GoogleResultsPageに"public void search(String keyword)"を追加する
  • GoogleResultsPageの"public void search(String keyword)"に"search.button.click()"を追加する
class GoogleResultsPage extends Page {
	static at = { title.endsWith "Google Search" }
	static content = {
		// reuse our previously defined module
		search{ module GoogleSearchModule, buttonName: "btnG" }

		// content definitions can compose and build from other definitions
		results { $("div.g") }
		result { i -> results[i] }
		resultLink { i -> result(i).find("a") }
		firstResultLink { resultLink(0) }
	}
	//firstResultLinkClickを追加
	public void firstResultLinkClick(){
		firstResultLink.click()
	}
        public void search(String keyword){
		try {
			search.field.value(keyword)
			search.button.click()
		} catch (Exception e) {
		}
	}
}


  • GoogleHomePageの"public void search(String keyword)"に"search.button.click()"を追加する
class GoogleHomePage extends Page {

	// pages can define their location, either absolutely or relative to a base
	static url = "http://google.com/ncr"

	// “at checkers” allow verifying that the browser is at the expected page
	static at = { title == "Google" }

	static content = {
		// include the previously defined module
		search{ module GoogleSearchModule, buttonName: "btnG" }
	}

	public void search(String keyword){
		try {
			search.field.value(keyword)
			search.button.click()
		} catch (Exception e) {
		}
	}
}
  • "waitFor { firstResultLink.text() == "Wikipedia"}"と"waitFor"を追記する
class GoogleWikipediaMainTest extends GebSpec {
	def "first result for wikipedia search should be wikipedia"() {
		given:
		to GoogleHomePage

		expect:
		at GoogleHomePage

		when:
		search("test")

		then:
		waitFor { at GoogleResultsPage }

		when:
		search("wikipedia")

		then:
		waitFor { at GoogleResultsPage }

		and:
		waitFor { firstResultLink.text() == "Wikipedia"}

		when:
		firstResultLinkClick()

		then:
		waitFor { at WikipediaPage }
	}
}

6. まとめ

  1. Spockに記述するテスト部分には実装(Module)を表に出さないようにする。pageオブジェクトのメソッドとして定義することでそれを実現する
    • 例:searchとしては、こうすることで変更に強い実装となる
      • "問題解決とリファクタリング(3)"のような"search.button.click()"は、pageオブジェクトのメソッドにしておいたことで、テストコードを汚すことなくリファクタすることができた
  2. 処理が早くてassertでエラーが発生する場合は、waitForを追加すること
    • 例: firstResultLink.text() == "Wikipedia" → waitFor { firstResultLink.text() == "Wikipedia"}

変更点まとめ(diff)

diff --git a/src/main/groovy/module/GoogleSearchModule.groovy b/src/main/groovy/module/GoogleSearchModule.groovy
index 7ed091e..96d33b1 100644
--- a/src/main/groovy/module/GoogleSearchModule.groovy
+++ b/src/main/groovy/module/GoogleSearchModule.groovy
@@ -6,7 +6,7 @@
 class GoogleSearchModule extends Module {
 
 	// a parameterised value set when the module is included
-	def buttonValue
+	def buttonName
 
 	// the content DSL
 	static content = {
@@ -15,9 +15,9 @@
 		field { $("input", name: "q") }
 
 		// the search button declares that it takes us to the results page, and uses the
-		// parameterised buttonValue to define itself
+		// parameterised buttonName to define itself
 		button(to: GoogleResultsPage) {
-			$("input", value: buttonValue)
+			$("button", name: buttonName)
 		}
 	}
 }
\ No newline at end of file
diff --git a/src/main/groovy/page/GoogleHomePage.groovy b/src/main/groovy/page/GoogleHomePage.groovy
index 5e5fdbf..0cb547b 100644
--- a/src/main/groovy/page/GoogleHomePage.groovy
+++ b/src/main/groovy/page/GoogleHomePage.groovy
@@ -13,6 +13,14 @@
 
 	static content = {
 		// include the previously defined module
-		search { module GoogleSearchModule, buttonValue: "Google Search" }
+		search{ module GoogleSearchModule, buttonName: "btnG" }
+	}
+
+	public void search(String keyword){
+		try {
+			search.field.value(keyword)
+			search.button.click()
+		} catch (Exception e) {
+		}
 	}
 }
\ No newline at end of file
diff --git a/src/main/groovy/page/GoogleResultsPage.groovy b/src/main/groovy/page/GoogleResultsPage.groovy
index aa50b09..bd25c5d 100644
--- a/src/main/groovy/page/GoogleResultsPage.groovy
+++ b/src/main/groovy/page/GoogleResultsPage.groovy
@@ -4,15 +4,24 @@
 import module.GoogleSearchModule
 
 class GoogleResultsPage extends Page {
+	static url = "https://www.google.com/?gws_rd=ssl"
 	static at = { title.endsWith "Google Search" }
 	static content = {
 		// reuse our previously defined module
-		search { module GoogleSearchModule, buttonValue: "Search" }
+		search{ module GoogleSearchModule, buttonName: "btnG" }
+		resultLinks(wait:true){ $("li" , class:"g")}
+		firstResultLink(wait:true){ $("li" , class:"g" , 0).$("a")}
+	}
+	//firstResultLinkClickを追加
+	public void firstResultLinkClick(){
+		firstResultLink.click()
+	}
 
-		// content definitions can compose and build from other definitions
-		results { $("div.g") }
-		result { i -> results[i] }
-		resultLink { i -> result(i).find("a") }
-		firstResultLink { resultLink(0) }
+	public void search(String keyword){
+		try {
+			search.field.value(keyword)
+			search.button.click()
+		} catch (Exception e) {
+		}
 	}
 }
diff --git a/src/test/groovy/main/GoogleWikipediaMainTest.groovy b/src/test/groovy/main/GoogleWikipediaMainTest.groovy
index 1590200..12559ea 100644
--- a/src/test/groovy/main/GoogleWikipediaMainTest.groovy
+++ b/src/test/groovy/main/GoogleWikipediaMainTest.groovy
@@ -6,7 +6,6 @@
 import page.WikipediaPage
 
 class GoogleWikipediaMainTest extends GebSpec {
-
 	def "first result for wikipedia search should be wikipedia"() {
 		given:
 		to GoogleHomePage
@@ -15,16 +14,22 @@
 		at GoogleHomePage
 
 		when:
-		search.field.value("wikipedia")
+		search("test")
+
+		then:
+		waitFor { at GoogleResultsPage }
+
+		when:
+		search("wikipedia")
 
 		then:
 		waitFor { at GoogleResultsPage }
 
 		and:
-		firstResultLink.text() == "Wikipedia"
+		waitFor { firstResultLink.text() == "Wikipedia"}
 
 		when:
-		firstResultLink.click()
+		firstResultLinkClick()
 
 		then:
 		waitFor { at WikipediaPage }