使用 Spock 的輔助方法減少重複
一、簡介
在編寫測試時,我們經常對物件的各種屬性進行斷言比較。 Spock 有一些有用的語言功能,可以幫助我們在比較物件時消除重複。
在本教程中,我們將學習如何使用 Spock 的輔助方法with()
和verifyAll(),
以使我們的測試更具可讀性。
2. 設定
首先,讓我們建立一個包含名稱、目前餘額和透支限額的Account
類別:
public class Account {
private String accountName;
private BigDecimal currentBalance;
private long overdraftLimit;
// getters and setters
}
3. 我們的基本測試
現在,讓我們建立一個AccountTest
Specification
,其中包含一個測試來驗證帳戶名稱、當前餘額和透支限額的 getter 和 setter。我們將將該Account
聲明為我們的@Subject
,並為每個測試建立一個新帳戶:
class AccountTest extends Specification {
@Subject
Account account = new Account()
def "given an account when we set its attributes then we get the same values back"() {
when: "we set attributes on our account"
account.setAccountName("My Account")
account.setCurrentBalance(BigDecimal.TEN)
account.setOverdraftLimit(0)
then: "the values we retrieve match the ones that we set"
account.getAccountName() == "My Account"
account.getCurrentBalance() == BigDecimal.TEN
account.getOverdraftLimit() == 0
}
}
在這裡,我們在期望中反覆引用了我們的account object
。我們需要比較的屬性越多,我們需要的重複次數就越多。
4. 重構選項
讓我們重構程式碼以刪除一些重複內容。
4.1.斷言陷阱
我們最初的嘗試可能會將 getter 比較提取到一個單獨的方法中:
void verifyAccount(Account accountToVerify) {
accountToVerify.getAccountName() == "My Account"
accountToVerify.getCurrentBalance() == BigDecimal.TEN
accountToVerify.getOverdraftLimit() == 0
}
但是,這有一個問題。讓我們透過建立一個verifyAccountRefactoringTrap
方法來比較我們的帳戶但使用其他人帳戶中的值來看看它是什麼:
void verifyAccountRefactoringTrap(Account accountToVerify) {
accountToVerify.getAccountName() == "Someone else's account"
accountToVerify.getCurrentBalance() == BigDecimal.ZERO
accountToVerify.getOverdraftLimit() == 9999
}
現在,讓我們在then
塊中呼叫我們的方法:
then: "the values we retrieve match the ones that we set"
verifyAccountRefactoringTrap(account)
當我們運行測試時,即使值不匹配,它也會通過!發生什麼事了?
儘管程式碼看起來像是在比較值,但我們的verifyAccountRefactoringTrap
方法包含布林表達式,但沒有斷言!
Spock 的隱式斷言僅在我們在測試方法中使用它們時發生,而不是在呼叫的方法內部發生。
那麼,我們該如何解決呢?
4.2.斷言內部方法
當我們將比較轉移到單獨的方法中時, Spock
無法再自動強制執行它們,因此我們必須自己添加assert
關鍵字。
因此,讓我們建立一個verifyAccountAsserted
方法來assert
我們的原始帳戶值:
void verifyAccountAsserted(Account accountToVerify) {
assert accountToVerify.getAccountName() == "My Account"
assert accountToVerify.getCurrentBalance() == BigDecimal.TEN
assert accountToVerify.getOverdraftLimit() == 0
}
讓我們在then
塊中呼叫verifyAccountAsserted
方法:
then: "the values we retrieve match the ones that we set"
verifyAccountAsserted(account)
當我們運行測試時,它仍然會通過,而當我們更改斷言值之一時,它會像我們預期的那樣失敗。
4.3.傳回一個布林值
確保我們的方法被視為斷言的另一種方法是返回布林結果。讓我們將斷言與布林值and
運算子結合起來,知道 Groovy 將傳回方法中最後執行的語句的結果:
boolean matchesAccount(Account accountToVerify) {
accountToVerify.getAccountName() == "My Account"
&& accountToVerify.getCurrentBalance() == BigDecimal.TEN
&& accountToVerify.getOverdraftLimit() == 0
}
當所有條件都匹配時,我們的測試就會通過;當一個或多個條件不匹配時,我們的測試就會失敗,但有一個缺點。當我們的測試失敗時,我們將不知道三個條件中的哪一個沒有被滿足。
雖然我們可以使用這些方法來重建我們的測試,但 Spock 的輔助方法為我們提供了更好的方法。
5. 輔助方法
Spock提供了兩個輔助方法「 with
」和「 verifyAll
」來幫助我們更優雅地解決問題!兩者的工作方式大致相同,所以讓我們從學習如何使用with.
5.1. with()
輔助方法
Spock 的with()
輔助方法接受一個物件和該物件的閉包。當我們將物件傳遞給輔助方法with()
時,物件的屬性和方法將會加入我們的上下文中。這意味著當我們在with()
閉包的範圍內時,我們不需要為它們添加物件名稱前綴。
因此,一種選擇是重構我們的方法以使用with:
void verifyAccountWith(Account accountToVerify) {
with(accountToVerify) {
getAccountName() == "My Account"
getCurrentBalance() == BigDecimal.TEN
getOverdraftLimit() == 0
}
}
請注意,強力斷言適用於 Spock 的輔助方法內部,因此我們不需要任何assert
,即使它位於單獨的方法中!
通常,我們甚至不需要單獨的方法,所以讓我們直接在測驗的期望中使用with()
:
then: "the values we retrieve match the ones that we set"
with(account) {
getAccountName() == "My Account"
getCurrentBalance() == BigDecimal.TEN
getOverdraftLimit() == 0
}
現在讓我們從斷言中呼叫我們的方法:
then: "the values we retrieve match the ones that we set"
verifyAccountWith(account)
我們的with()
方法通過了,但在第一次不匹配的比較時未通過測試。
5.2.用於模擬
我們也可以在斷言互動時使用with
。讓我們建立一個Mock Account
並呼叫它的一些設定器:
given: 'a mock account'
Account mockAccount = Mock()
when: "we invoke its setters"
mockAccount.setAccountName("A Name")
mockAccount.setOverdraftLimit(0)
現在讓我們驗證一些使用with
我們的mockAccount
交互作用:
with(mockAccount) {
1 * setAccountName(_ as String)
1 * setOverdraftLimit(_)
}
請注意,在驗證mockAccount.setAccountName
時,我們可以省略mockAccount
因為它在我們的with
範圍內。
5.3. verifyAll()
輔助方法
有時我們寧願知道執行測試時失敗的每個斷言。在這種情況下,我們可以使用 Spock 的verifyAll()
輔助方法,與我們使用with.
因此,讓我們使用verifyAll()
新增檢查:
verifyAll(accountToVerify) {
getAccountName() == "My Account"
getCurrentBalance() == BigDecimal.TEN
getOverdraftLimit() == 0
}
當一個比較失敗時, verifyAll()
方法將繼續執行並報告verifyAll's
範圍內的所有失敗比較,而不是使測試失敗。
5.4.嵌套輔助方法
當我們要比較物件中的物件時,我們可以嵌套輔助方法。
要了解如何先建立一個包含街道和城市的Address
並將其添加到我們的Account:
public class Address {
String street;
String city;
// getters and setters
}
public class Account {
private Address address;
// getter and setter and rest of class
}
現在我們已經有了一個Address
類,讓我們在測試中建立一個:
given: "an address"
Address myAddress = new Address()
def myStreet = "1, The Place"
def myCity = "My City"
myAddress.setStreet(myStreet)
myAddress.setCity(myCity)
並將其添加到我們的account
:
when: "we set attributes on our account"
account.setAddress(myAddress)
account.setAccountName("My Account")
接下來,我們來比較一下我們的地址。當我們不使用輔助方法時,我們最基本的方法是:
account.getAddress().getStreet() == myStreet
account.getAddress().getCity() == myCity
我們可以透過提取address
變數來改進這一點:
def address = account.getAddress()
address.getCity() == myCity
address.getStreet() == myStreet
但更好的是,讓我們使用with
輔助方法進行更清晰的比較:
with(account.getAddress()) {
getStreet() == myStreet
getCity() == myCity
}
現在我們使用with
來比較我們的地址,讓我們將其嵌套在帳戶的比較中。由於with(account)
將我們的account
納入範圍,因此我們可以將其從account.getAddress()
中刪除並使用with(getAddress())
代替:
then: "the values we retrieve match the ones that we set"
with(account) {
getAccountName() == "My Account"
with(getAddress()) {
getStreet() == myStreet
getCity() == myCity
}
}
由於 Groovy 可以為我們派生 getter 和 setter,因此我們也可以透過 name 引用物件的屬性。
因此,讓我們透過使用屬性名稱而不是 getter 來使我們的測試更具可讀性:
with(account) {
accountName == "My Account"
with(address) {
street == myStreet
city == myCity
}
}
6. 它們如何運作?
我們已經了解了with()
和verifyAll()
如何幫助我們讓測驗更具可讀性,但它們是如何做到這一點的呢?
讓我們來看看with()
的方法簽章:
with(Object, Closure)
所以我們可以透過傳遞一個Closure
作為第二個參數來使用with()
:
with(account, (acct) -> {
acct.getAccountName() == "My Account"
acct.getOverdraftLimit() == 0
})
但 Groovy 對最後一個參數是Closure.
它允許我們在括號之外聲明Closure
。
因此,讓我們使用 Groovy 的更易讀的等效形式,但更簡潔的形式:
with(account) {
getAccountName() == "My Account"
getOverdraftLimit() == 0
}
請注意,在我們的測試中,有兩個參數,但我們只將一個參數account,
傳遞給with().
第二個Closure
參數是緊接在with
之後的大括號內的程式碼,它透過第一個account
參數來運作。
七、結論
在本教學中,我們學習如何使用 Spock 的with()
和verifyAll()
輔助方法來減少比較物件時測試中的樣板檔案。我們學習如何將輔助方法用於簡單的對象,以及如何在物件更複雜時進行嵌套。我們也了解如何使用 Groovy 的屬性表示法使我們的輔助斷言更加清晰,以及如何將輔助方法與Mocks
結合使用。最後,我們了解了在使用輔助方法時如何從 Groovy 的替代的、更清晰的語法中獲益,這些方法的最後一個參數是閉包。
與往常一樣,本教學的源代碼位於 GitHub 上。