測試指南

1. 簡介

Groovy 程式語言提供強大的測試撰寫支援。除了語言功能和與最先進的測試函式庫和架構進行測試整合之外,Groovy 生態系統還孕育了豐富的測試函式庫和架構。

本章節將從特定語言的測試功能開始,然後深入探討 JUnit 整合、用於規格的 Spock 以及用於功能測試的 Geb。最後,我們將概述已知與 Groovy 搭配使用的其他測試函式庫。

2. 語言功能

除了對 JUnit 的整合支援外,Groovy 程式語言還具備一些已被證明對測試驅動開發非常有價值的功能。本節將深入探討這些功能。

2.1. 強力斷言

撰寫測試表示使用斷言來制定假設。在 Java 中,這可透過使用 J2SE 1.4 中新增的 assert 關鍵字來完成。在 Java 中,assert 陳述可透過 JVM 參數 -ea(或 -enableassertions)和 -da(或 -disableassertions)啟用。Java 中的斷言陳述預設為停用。

Groovy 附帶一個 assert功能強大的變體,也稱為功能強大的斷言陳述。Groovy 的功能強大 assert 與 Java 版本不同,在於其輸出給定布林表達式驗證為 false

def x = 1
assert x == 2

// Output:             (1)
//
// Assertion failed:
// assert x == 2
//        | |
//        1 false
1 此部分顯示 std-err 輸出

每當無法成功驗證斷言時所擲回的 java.lang.AssertionError,包含原始例外狀況訊息的延伸版本。功能強大的斷言輸出顯示從外部到內部表達式的評估結果。

功能強大的斷言陳述真正的威力在於複雜的布林陳述,或具有集合或其他已啟用 toString 類別的陳述

def x = [1,2,3,4,5]
assert (x << 6) == [6,7,8,9,10]

// Output:
//
// Assertion failed:
// assert (x << 6) == [6,7,8,9,10]
//         | |     |
//         | |     false
//         | [1, 2, 3, 4, 5, 6]
//         [1, 2, 3, 4, 5, 6]

與 Java 的另一個重要差異在於,Groovy 中的斷言預設為啟用。移除停用斷言的可能性是一項語言設計決策。或者,正如 Bertrand Meyer 所述,如果你將腳放入真正的水中,脫掉你的游泳圈沒有任何意義

需要留意的一件事是功能強大的斷言陳述中布林表達式內的具有副作用的方法。由於內部錯誤訊息建構機制僅儲存對目標下實例的參照,因此會發生在涉及產生副作用的方法時,錯誤訊息文字在呈現時間無效的情況

assert [[1,2,3,3,3,3,4]].first().unique() == [1,2,3]

// Output:
//
// Assertion failed:
// assert [[1,2,3,3,3,3,4]].first().unique() == [1,2,3]
//                          |       |        |
//                          |       |        false
//                          |       [1, 2, 3, 4]
//                          [1, 2, 3, 4]           (1)
1 錯誤訊息顯示集合的實際狀態,而不是套用 unique 方法之前的狀態
如果你選擇提供自訂斷言錯誤訊息,可透過使用 Java 語法 assert expression1 : expression2 來完成,其中 expression1 是布林表達式,而 expression2 是自訂錯誤訊息。不過請注意,這會停用功能強大的斷言,並在斷言錯誤時完全回歸自訂錯誤訊息。

2.2. 模擬和存根

Groovy 內建了對各種模擬和存根替代方案的出色支援。在使用 Java 時,動態模擬架構非常受歡迎。其主要原因在於,使用 Java 建立自訂手動模擬非常困難。如果你選擇,可以使用 Groovy 輕鬆使用此類架構,但在 Groovy 中建立自訂模擬容易得多。你通常可以用簡單的對應或閉包來建立你的自訂模擬。

下列部分顯示僅使用 Groovy 語言功能建立模擬和存根的方法。

2.2.1. Map 轉換

藉由使用 map 或 expandos,我們可以很輕鬆地納入合作者的所需行為,如下所示

class TranslationService {
    String convert(String key) {
        return "test"
    }
}

def service = [convert: { String key -> 'some text' }] as TranslationService
assert 'some text' == service.convert('key.text')

as 算子可用於將 map 轉換為特定類別。已給定的 map 鍵會被解釋為方法名稱,而值為 groovy.lang.Closure 區塊,會被解釋為方法程式碼區塊。

請注意,如果你處理自訂的 java.util.Map 後代類別,並搭配使用 as 算子,map 轉換可能會造成阻礙。map 轉換機制直接針對特定收集類別,不會考慮自訂類別。

2.2.2. Closure 轉換

as 算子可以用在 closure 上,這是一個很不錯的方式,非常適合開發人員在簡單情境中進行測試。我們尚未發現這項技術強大到可以取代動態模擬,但它在簡單案例中仍然非常有用。

包含單一方法的類別或介面,包括 SAM (單一抽象方法) 類別,可用於將 closure 區塊轉換為給定類型的物件。請注意,為了執行此操作,Groovy 會在內部建立一個代理物件,繼承給定的類別。因此,物件不會是給定類別的直接執行個體。這很重要,例如,如果產生代理物件的元類別在之後被變更。

讓我們來看一個將 closure 轉換為特定類型的範例

def service = { String key -> 'some text' } as TranslationService
assert 'some text' == service.convert('key.text')

Groovy 支援一項稱為隱式 SAM 轉換的功能。這表示在執行階段可以推論出目標 SAM 類型的狀況下,as 算子並非必要。這種類型的轉換可能有助於在測試中模擬整個 SAM 類別

abstract class BaseService {
    abstract void doSomething()
}

BaseService service = { -> println 'doing something' }
service.doSomething()

2.2.3. MockFor 和 StubFor

Groovy 模擬和 stubbing 類別可以在 groovy.mock.interceptor 套件中找到。

MockFor 類別支援孤立地 (通常是單元) 測試類別,方法是允許定義合作者行為的嚴格順序預期。典型的測試情境包括一個受測類別和一個或多個合作者。在這種情境中,通常只希望測試受測類別的商業邏輯。執行此操作的一種策略是使用簡化的模擬物件取代合作者執行個體,以協助隔離測試目標中的邏輯。MockFor 允許使用元程式設計建立此類模擬。合作者的所需行為會定義為行為規範。系統會自動強制執行並檢查行為。

讓我們假設我們的目標類別如下所示

class Person {
    String first, last
}

class Family {
    Person father, mother
    def nameOfMother() { "$mother.first $mother.last" }
}

使用 MockFor 時,模擬預期總是依序而定,而且其使用會自動在呼叫 verify 時結束

def mock = new MockFor(Person)      (1)
mock.demand.getFirst{ 'dummy' }
mock.demand.getLast{ 'name' }
mock.use {                          (2)
    def mary = new Person(first:'Mary', last:'Smith')
    def f = new Family(mother:mary)
    assert f.nameOfMother() == 'dummy name'
}
mock.expect.verify()                (3)
1 新的模擬是由 MockFor 的新實例建立的
2 Closure 傳遞給 use,以啟用模擬功能
3 呼叫 verify 來檢查方法呼叫的順序和數量是否符合預期

StubFor 類別支援孤立地(通常是單元)測試類別,方法是允許定義合作者行為的鬆散順序預期。典型的測試情境涉及受測類別和一個或多個合作者。在這種情境中,通常只希望測試 CUT 的商業邏輯。執行此操作的一種策略是使用簡化的存根物件取代合作者實例,以協助孤立目標類別中的邏輯。StubFor 允許使用元程式設計建立此類存根。合作者的預期行為定義為行為規格。

MockFor 相反,使用 verify 檢查的存根預期與順序無關,而且其使用是選用的

def stub = new StubFor(Person)      (1)
stub.demand.with {                  (2)
    getLast{ 'name' }
    getFirst{ 'dummy' }
}
stub.use {                          (3)
    def john = new Person(first:'John', last:'Smith')
    def f = new Family(father:john)
    assert f.father.first == 'dummy'
    assert f.father.last == 'name'
}
stub.expect.verify()                (4)
1 新的存根是由 StubFor 的新實例建立的
2 with 方法用於將封閉中的所有呼叫委派給 StubFor 實例
3 Closure 傳遞給 use,以啟用存根功能
4 呼叫 verify(選用)來檢查方法呼叫的數量是否符合預期

MockForStubFor 無法用於測試靜態編譯的類別,例如使用 @CompileStatic 的 Java 類別或 Groovy 類別。若要存根和/或模擬這些類別,可以使用 Spock 或其中一個 Java 模擬函式庫。

2.2.4. Expando Meta-Class (EMC)

Groovy 包含一個特殊的 MetaClass,稱為 ExpandoMetaClass (EMC)。它允許使用簡潔的封閉語法動態新增方法、建構函式、屬性和靜態方法。

每個 java.lang.Class 都提供一個特殊的 metaClass 屬性,它會提供對 ExpandoMetaClass 實例的參考。Expando MetaClass 不限於自訂類別,它也可以用於 JDK 類別,例如 java.lang.String

String.metaClass.swapCase = {->
    def sb = new StringBuffer()
    delegate.each {
        sb << (Character.isUpperCase(it as char) ? Character.toLowerCase(it as char) :
            Character.toUpperCase(it as char))
    }
    sb.toString()
}

def s = "heLLo, worLD!"
assert s.swapCase() == 'HEllO, WORld!'

ExpandoMetaClass 是一個相當好的模擬功能候選,因為它允許更進階的內容,例如模擬靜態方法

class Book {
    String title
}

Book.metaClass.static.create << { String title -> new Book(title:title) }

def b = Book.create("The Stand")
assert b.title == 'The Stand'

甚至建構函式

Book.metaClass.constructor << { String title -> new Book(title:title) }

def b = new Book("The Stand")
assert b.title == 'The Stand'
模擬建構函式可能看起來像一個甚至最好不要考慮的技巧,但即使在那裡也可能有有效的用例。可以在 Grails 中找到一個範例,其中網域類別建構函式會在執行階段使用 ExpandoMetaClass 加入。這讓網域物件可以在 Spring 應用程式內容中註冊自己,並允許注入服務或由依賴注入容器控制的其他 bean。

如果你想在每個測試方法層級變更 metaClass 屬性,你需要移除對 metaclass 所做的變更,否則這些變更會在測試方法呼叫之間持續存在。變更會透過在 GroovyMetaClassRegistry 中替換 metaclass 來移除

GroovySystem.metaClassRegistry.removeMetaClass(String)

另一個替代方案是註冊一個 MetaClassRegistryChangeEventListener,追蹤變更的類別,並在所選測試執行階段的清除方法中移除變更。可以在 Grails 網路開發架構 中找到一個好的範例。

除了在類別層級使用 ExpandoMetaClass 之外,還支援在每個物件層級使用 metaclass

def b = new Book(title: "The Stand")
b.metaClass.getTitle {-> 'My Title' }

assert b.title == 'My Title'

在這種情況下,metaclass 變更僅與執行個體相關。根據測試情境,這可能比全域 metaclass 變更更合適。

2.3. GDK 方法

下列區段簡要說明可以在測試案例情境中運用的 GDK 方法,例如用於測試資料產生。

2.3.1. Iterable#combinations

在相容於 java.lang.Iterable 的類別上新增的 combinations 方法可用於從包含兩個或兩個以上子清單的清單中取得組合清單

void testCombinations() {
    def combinations = [[2, 3],[4, 5, 6]].combinations()
    assert combinations == [[2, 4], [3, 4], [2, 5], [3, 5], [2, 6], [3, 6]]
}

可以在測試案例情境中使用該方法來產生特定方法呼叫的所有可能的引數組合。

2.3.2. Iterable#eachCombination

eachCombination 方法新增於 java.lang.Iterable,可用於將函式(或在此情況下為 groovy.lang.Closure)套用至 combinations 方法所建立的每個組合

eachCombination 為 GDK 方法,新增至符合 java.lang.Iterable 介面的所有類別。它會將函式套用至輸入清單的每個組合

void testEachCombination() {
    [[2, 3],[4, 5, 6]].eachCombination { println it[0] + it[1] }
}

此方法可用於測試環境中,以每個產生的組合呼叫方法。

2.4. 工具支援

2.4.1. 測試程式碼涵蓋率

程式碼涵蓋率是衡量(單元)測試效能的實用指標。程式碼涵蓋率高的程式,發生重大錯誤的機率低於涵蓋率低或沒有涵蓋率的程式。若要取得程式碼涵蓋率指標,通常需要在執行測試前,對產生的位元組碼進行編制。支援 Groovy 執行此任務的其中一個工具為 Cobertura

各種架構和建置工具都整合了 Cobertura。對於 Grails,有基於 Cobertura 的 程式碼涵蓋率外掛程式,對於 Gradle,則有 gradle-cobertura 外掛程式,僅舉兩個例子。

以下程式碼清單顯示如何從 Groovy 專案的 Gradle 建置指令碼中啟用 Cobertura 測試涵蓋率報告

def pluginVersion = '<plugin version>'
def groovyVersion = '<groovy version>'
def junitVersion = '<junit version>'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.eriwen:gradle-cobertura-plugin:${pluginVersion}'
    }
}

apply plugin: 'groovy'
apply plugin: 'cobertura'

repositories {
    mavenCentral()
}

dependencies {
    compile "org.codehaus.groovy:groovy-all:${groovyVersion}"
    testCompile "junit:junit:${junitVersion}"
}

cobertura {
    format = 'html'
    includes = ['**/*.java', '**/*.groovy']
    excludes = ['com/thirdparty/**/*.*']
}

可以為 Cobertura 涵蓋率報告選擇多種輸出格式,並可以將測試程式碼涵蓋率報告新增至持續整合建置任務。

3. 使用 JUnit 進行測試

Groovy 透過以下方式簡化 JUnit 測試

  • 您在使用 Java 進行測試時使用相同的整體做法,但可以在測試中採用 Groovy 的簡潔語法,讓測試更簡潔。如果您有興趣,甚至可以使用撰寫測試領域特定語言 (DSL) 的功能。

  • 有許多輔助類別可以簡化許多測試活動。在某些情況下,詳細資料會因您使用的 JUnit 版本而異。我們將在稍後介紹這些詳細資料。

  • Groovy 的 PowerAssert 機制非常適合用於您的測試

  • Groovy 認為測試非常重要,您應該能夠像執行指令碼或類別一樣輕鬆地執行測試。這就是為什麼在使用 groovy 指令或 GroovyConsole 時,Groovy 會包含一個自動測試執行器。這會為您提供一些額外的選項,讓您不僅可以執行測試

在以下各節中,我們將深入探討 JUnit 3、4 和 5 Groovy 整合。

3.1. JUnit 3

支援 JUnit 3 測試的最重要的 Groovy 類別之一可能是 GroovyTestCase 類別。由於衍生自 junit.framework.TestCase,因此它提供了許多額外的方法,讓 Groovy 中的測試變得輕而易舉。

雖然 GroovyTestCase 繼承自 TestCase,並不表示您無法在專案中使用 JUnit 4 功能。事實上,最新版本的 Groovy 內建 JUnit 4,並附帶向後相容的 TestCase 實作。Groovy 郵件清單中曾討論過要使用 GroovyTestCase 還是 JUnit 4,結果發現這主要取決於個人喜好,但透過 GroovyTestCase,您可以免費取得許多方法,讓特定類型的測試更容易撰寫。

在本節中,我們將探討 GroovyTestCase 提供的一些方法。您可以在 groovy.test.GroovyTestCase 的 JavaDoc 文件中找到這些方法的完整清單,別忘了它繼承自 junit.framework.TestCase,繼承了所有 assert* 方法。

3.1.1. 斷言方法

GroovyTestCase 繼承自 junit.framework.TestCase,因此它繼承了大量的斷言方法,可以在每個測試方法中呼叫

class MyTestCase extends GroovyTestCase {

    void testAssertions() {
        assertTrue(1 == 1)
        assertEquals("test", "test")

        def x = "42"
        assertNotNull "x must not be null", x
        assertNull null

        assertSame x, x
    }

}

如上所示,與 Java 不同,在大多數情況下,您可以在括號中省略括號,這讓 JUnit 斷言方法呼叫表達式的可讀性更高。

GroovyTestCase 新增了一個有趣的斷言方法,稱為 assertScript。它確保指定的 Groovy 程式碼字串在沒有任何例外情況下執行成功

void testScriptAssertions() {
    assertScript '''
        def x = 1
        def y = 2

        assert x + y == 3
    '''
}

3.1.2. shouldFail 方法

shouldFail 可用於檢查指定的程式碼區塊是否失敗。如果失敗,則斷言成立,否則斷言失敗

void testInvalidIndexAccess1() {
    def numbers = [1,2,3,4]
    shouldFail {
        numbers.get(4)
    }
}

上面的範例使用基本的 shouldFail 方法介面,將 groovy.lang.Closure 作為單一引數。Closure 執行個體包含在執行期間應該中斷的程式碼。

如果我們想針對特定 java.lang.Exception 類型斷言 shouldFail,我們可以使用將 Exception 類別作為第一個引數,將 Closure 作為第二個引數的 shouldFail 實作

void testInvalidIndexAccess2() {
    def numbers = [1,2,3,4]
    shouldFail IndexOutOfBoundsException, {
        numbers.get(4)
    }
}

如果拋出除了 IndexOutOfBoundsException(或其子類別)以外的任何例外,測試案例將會失敗。

shouldFail 的一個相當不錯的功能到目前為止尚未可見:它會傳回例外訊息。如果您想要斷言例外錯誤訊息,這會非常有用

void testInvalidIndexAccess3() {
    def numbers = [1,2,3,4]
    def msg = shouldFail IndexOutOfBoundsException, {
        numbers.get(4)
    }
    assert msg.contains('Index: 4, Size: 4') ||
        msg.contains('Index 4 out-of-bounds for length 4') ||
        msg.contains('Index 4 out of bounds for length 4')
}

3.1.3. notYetImplemented 方法

notYetImplemented 方法受到 HtmlUnit 的極大影響。它允許撰寫測試方法,但將其標記為尚未實作。只要測試方法失敗並標記為 notYetImplemented,測試就會通過

void testNotYetImplemented1() {
    if (notYetImplemented()) return   (1)

    assert 1 == 2                     (2)
}
1 GroovyTestCase 需要呼叫 notYetImplemented 來取得目前的堆疊方法。
2 只要測試評估為 false,測試執行就會成功。

notYetImplemented 方法的替代方法是 @NotYetImplemented 註解。它允許將方法註解為尚未實作,其行為與 GroovyTestCase#notYetImplemented 完全相同,但不需要呼叫 notYetImplemented 方法

@NotYetImplemented
void testNotYetImplemented2() {
    assert 1 == 2
}

3.2. JUnit 4

Groovy 可用於撰寫 JUnit 4 測試案例,沒有任何限制。groovy.test.GroovyAssert 包含各種靜態方法,可用於取代 JUnit 4 測試中的 GroovyTestCase 方法

import org.junit.Test

import static groovy.test.GroovyAssert.shouldFail

class JUnit4ExampleTests {

    @Test
    void indexOutOfBoundsAccess() {
        def numbers = [1,2,3,4]
        shouldFail {
            numbers.get(4)
        }
    }

}

如上方的範例所示,在類別定義的開頭會匯入 GroovyAssert 中的靜態方法,因此 shouldFail 的使用方式與在 GroovyTestCase 中相同。

groovy.test.GroovyAssert 繼承自 org.junit.Assert,這表示它繼承了所有 JUnit 斷言方法。然而,隨著 power 斷言陳述的推出,證明了依賴斷言陳述是良好的實務,而不是使用 JUnit 斷言方法,而改進的訊息是主要原因。

值得一提的是,GroovyAssert.shouldFail 並不完全等同於 GroovyTestCase.shouldFailGroovyTestCase.shouldFail 會傳回例外訊息,而 GroovyAssert.shouldFail 則會傳回例外本身。雖然取得訊息需要多按幾下鍵,但相對地,您可以存取例外的其他屬性和方法

@Test
void shouldFailReturn() {
    def e = shouldFail {
        throw new RuntimeException('foo',
                                   new RuntimeException('bar'))
    }
    assert e instanceof RuntimeException
    assert e.message == 'foo'
    assert e.cause.message == 'bar'
}

3.3. JUnit 5

使用 JUnit5 時,JUnit4 所述的大部分方法和輔助類別都適用,不過 JUnit5 在撰寫測試時會使用一些略有不同的類別註解。有關詳細資訊,請參閱 JUnit5 文件。

按照正常的 JUnit5 指南建立您的測試類別,如下例所示

class MyTest {
  @Test
  void streamSum() {
    assertTrue(Stream.of(1, 2, 3)
      .mapToInt(i -> i)
      .sum() > 5, () -> "Sum should be greater than 5")
  }

  @RepeatedTest(value=2, name = "{displayName} {currentRepetition}/{totalRepetitions}")
  void streamSumRepeated() {
    assert Stream.of(1, 2, 3).mapToInt(i -> i).sum() == 6
  }

  private boolean isPalindrome(s) { s == s.reverse()  }

  @ParameterizedTest                                                              (1)
  @ValueSource(strings = [ "racecar", "radar", "able was I ere I saw elba" ])
  void palindromes(String candidate) {
    assert isPalindrome(candidate)
  }

  @TestFactory
  def dynamicTestCollection() {[
    dynamicTest("Add test") { -> assert 1 + 1 == 2 },
    dynamicTest("Multiply Test", () -> { assert 2 * 3 == 6 })
  ]}
}
1 如果專案中尚未包含,此測試需要額外的 org.junit.jupiter:junit-jupiter-params 相依性。

如果您使用 IDE 或建置工具,且該工具支援 JUnit5 並已針對其進行設定,則可以在其中執行測試。如果您在 GroovyConsole 或透過 groovy 指令執行上述測試,您會看到測試執行結果的簡短文字摘要

JUnit5 launcher: passed=8, failed=0, skipped=0, time=246ms

更詳細的資訊可在 FINE 記錄層級中取得。您可以設定您的記錄以顯示此類資訊,或以程式設計方式執行,如下所示

@BeforeAll
static void init() {
  def logger = Logger.getLogger(LoggingListener.name)
  logger.level = Level.FINE
  logger.addHandler(new ConsoleHandler(level: Level.FINE))
}

4. 使用 Spock 進行測試

Spock 是 Java 和 Groovy 應用程式的測試和規範架構。它之所以脫穎而出,在於其漂亮且極具表現力的規範 DSL。在實務上,Spock 規範是以 Groovy 類別撰寫的。儘管是以 Groovy 撰寫,但它們可用於測試 Java 類別。Spock 可用於單元、整合或 BDD (行為驅動開發) 測試,它並未將自己歸類為特定類別的測試架構或函式庫。

除了這些令人驚豔的功能之外,Spock 也是一個很好的範例,說明如何在第三方函式庫中運用進階 Groovy 程式語言功能,例如透過使用 Groovy AST 轉換。
本節不應作為如何使用 Spock 的詳細指南,而應讓您了解 Spock 的用途,以及如何將其運用於單元、整合、功能或任何其他類型的測試。

在下一節中,我們將首次了解 Spock 規範的解剖結構。它應該能讓人對 Spock 的作用有一個相當好的感覺。

4.1. 規範

Spock 讓您可以撰寫規範,用以描述由感興趣的系統所展示的功能(屬性、面向)。「系統」可以是從單一類別到整個應用程式的任何東西,一個較進階的術語是規範下的系統功能描述從系統及其合作者的特定快照開始,這個快照稱為功能固定裝置

Spock 規範類別衍生自 spock.lang.Specification。具體的規範類別可能包含欄位、固定裝置方法、功能方法和輔助方法。

讓我們來看一個針對虛擬 Stack 類別的簡單規範,其中包含一個功能方法

class StackSpec extends Specification {

    def "adding an element leads to size increase"() {  (1)
        setup: "a new stack instance is created"        (2)
            def stack = new Stack()

        when:                                           (3)
            stack.push 42

        then:                                           (4)
            stack.size() == 1
    }
}
1 功能方法,依慣例以字串文字命名。
2 設定區塊,這是需要為此功能執行任何設定工作的地方。
3 When 區塊描述一個刺激,這是此功能規範針對目標執行的特定動作。
4 Then 區塊任何可以用來驗證由 When 區塊觸發的程式碼結果的表達式。

Spock 功能規範定義為 spock.lang.Specification 類別內的函式。它們使用字串文字而非函式名稱來描述功能。

一個功能方法包含多個區塊,在我們的範例中,我們使用了 setupwhenthensetup 區塊特別在於它是選用的,且允許設定在功能方法內可見的區域變數。when 區塊定義刺激,並且是 then 區塊的伴侶,後者描述對刺激的回應。

請注意,上述 StackSpec 中的 setup 方法另外有一個描述字串。描述字串是選用的,且可以在區塊標籤(例如 setupwhenthen)後加入。

4.2. 更多 Spock

Spock 提供更多功能,例如資料表格或進階模擬功能。歡迎參閱 Spock GitHub 頁面,以取得更多文件和下載資訊。

5. 使用 Geb 進行功能測試

Geb 是與 JUnit 和 Spock 整合的功能性網路測試和刮取程式庫。它基於 Selenium 網路驅動程式,並像 Spock 一樣提供 Groovy DSL,用於為網路應用程式撰寫功能測試。

Geb 具有多項優異的功能,使其非常適合作為功能測試程式庫

  • 透過類似 JQuery 的 $ 函數存取 DOM

  • 實作頁面模式

  • 透過模組支援特定網路元件(例如功能表列等)的模組化

  • 透過 JS 變數與 JavaScript 整合

本節不應作為如何使用 Geb 的詳細指南,而應說明 Geb 的用途以及如何利用它進行功能測試。

下一節將提供一個範例,說明如何使用 Geb 為具有單一搜尋欄位的簡單網頁撰寫功能測試。

5.1. Geb 程式碼

雖然 Geb 可以獨立用於 Groovy 程式碼中,但在許多情況下,它會與其他測試架構搭配使用。Geb 附帶各種基本類別,可於 JUnit 3、4、TestNG 或 Spock 測試中使用。這些基本類別是額外 Geb 模組的一部分,需要新增為相依性。

例如,以下 @Grab 相依性可用於在 JUnit4 測試中使用 Selenium Firefox 驅動程式執行 Geb。支援 JUnit 3/4 的模組為 geb-junit4

@Grab('org.gebish:geb-core:0.9.2')
@Grab('org.gebish:geb-junit4:0.9.2')
@Grab('org.seleniumhq.selenium:selenium-firefox-driver:2.26.0')
@Grab('org.seleniumhq.selenium:selenium-support:2.26.0')

Geb 中的核心類別是 geb.Browser 類別。顧名思義,它用於瀏覽頁面和存取 DOM 元素

import geb.Browser
import org.openqa.selenium.firefox.FirefoxDriver

def browser = new Browser(driver: new FirefoxDriver(), baseUrl: 'http://myhost:8080/myapp')  (1)
browser.drive {
    go "/login"                        (2)

    $("#username").text = 'John'       (3)
    $("#password").text = 'Doe'

    $("#loginButton").click()

    assert title == "My Application - Dashboard"
}
1 建立新的 Browser 執行個體。在此情況下,它使用 Selenium FirefoxDriver 並設定 baseUrl
2 go 用於導覽至 URL 或相對 URI
3 $ 與 CSS 選擇器一起使用,用於存取 usernamepassword DOM 欄位。

Browser 類別附帶一個 drive 方法,它會將所有方法/屬性呼叫委派給目前的 browser 執行個體。Browser 組態不得內嵌,也可以外置在 GebConfig.groovy 組態檔案中,例如。實際上,Browser 類別的使用大多隱藏在 Geb 測試基礎類別中。它們將所有遺失的屬性和方法呼叫委派給存在於背景中的目前 browser 執行個體

class SearchTests extends geb.junit4.GebTest {

    @Test
    void executeSeach() {
        go 'http://somehost/mayapp/search'              (1)
        $('#searchField').text = 'John Doe'             (2)
        $('#searchButton').click()                      (3)

        assert $('.searchResult a').first().text() == 'Mr. John Doe' (4)
    }
}
1 Browser#go 會取得相對或絕對連結並呼叫頁面。
2 Browser#$ 用於存取 DOM 內容。允許使用底層 Selenium 驅動程式支援的任何 CSS 選擇器
3 click 用於按一下按鈕。
4 $ 用於取得 searchResult 區塊中的第一個連結

以上的範例顯示一個簡單的 Geb 網路測試,其 JUnit 4 基底類別為 geb.junit4.GebTest。請注意,在此情況下,Browser 組態是外置的。GebTestgo$ 等方法委派給底層的 browser 執行個體。

5.2. 更多 Geb

在上一節中,我們僅觸及可用 Geb 功能的皮毛。可以在 專案首頁 找到更多關於 Geb 的資訊。