軟體工程發生在代碼被非原作者閱讀之時
Spock vs JUnit
單元測試架構,JUnit讀者已了解,是以直接開門見山,基于JUnit和Spock做一個對比,明顯Spock在工程化更有優勢。
對比項 | Spock | JUnit |
結構可讀性 | ✓ | |
問題定位效率 | ||
報告可讀性 | ||
參數化測試 | ||
Mock能力 |
Spock強制使用一套清晰的測試結構,而JUnit測試缺乏正式的語義。使用Spock的首要原因就是它可以使得你的測試更具可讀性。如果你隻是在周末業餘時間建立一些小的項目,那這一點看起來就不那麼重要了。然而對于大型的企業級項目來說,當你要在原有的代碼基礎上工作時,測試的可讀性就顯得猶為重要了。
如果你檢視那些優秀的JUnit測試,你就會發現在它的結構裡會有一種模式。這種結構就是非常著名的arrange-act-assert 模式,它将測試清晰的分為三個階段,如下所示:
@Test
public void chargeCreditCard() {
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
billing.charge(client,150);
assertEquals("Revenue should be recorded",150,billing.getCurrentRevenue());
}
其中arrange 階段準備好了所需的類。
在上面的例子中,前兩個語句就屬于這個階段(一個用戶端和一個計費的服務對象被初始化)。act 是測試的觸發。這個階段應該會調用測試下面的主題的方法。在我們的用例中,主題就是計費服務。觸發就是150美元的收費。assert 階段是最後一步,它将觸發的實際結果與我們期待的做對比。如果它們一緻,則測試就成功了,否則測試就是失敗的。
這個arrange-act-assert 結構隻有開發人員考慮使用它時才會存在(也就是說并不是所有人都會用)。
上面的例子非常的簡單,是以三個階段就很容易看出來。我們可以對該測試進行改善,也就是如下所示的加上空行:
@Test
public void chargeCreditCard() {
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
billing.charge(client,150);
assertEquals("Revenue should be recorded",150,billing.getCurrentRevenue());
}
然而在一些相當大的測試中,這些階段并不總是那麼明顯。設想有一個大的JUnit測試,使用了複雜的業務邏輯。經驗豐富的開發人員仍然會通過使用注釋來辨別出各個階段,以便于提高其可讀性:
@Test
public void veryComplexLoanApprovalScenario() {
//Prepare loan request and client details
[...lots of statements here...]
//Customer asks for a loan
[...lots of statements here...]
//Check that loan was approved
[...lots of assert statements here...]
}
注釋的使用當然會使測試更加容易讀懂了,但是這種技術遠不是我們所理想的。
首先,大型的JUnit測試都是反模式的,都應該被重構使用私有的方法來滿足業務需求。
其次,并非所有的開發人員都會注釋,而這些注釋也隻能由程式員閱讀到(比如說,它們永遠不會出現在測試報告上)。
Spock測試使得arrange-act-assert結構顯而易見,讓我們使用Spock來重寫下第一個測試:
public void "charging a credit card - happy path"() {
given: "a billing service and a customer with a valid credit card"
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
when: "client buys something with 150 dollars"
billing.charge(client,150);
then: "we expect the transaction to be recorded"
billing.getCurrentRevenue() == 150
}
盡管該代碼片段是采用Groovy書寫的,我還是盡量保證跟Java相似些。首先你會注意到方法的名字是一段英國文字,明确的描述了該測試所要做的事情(之後會有更多說明)。其次要注意的是一堆given-when-then的标簽将代碼分成了三部分。很明顯的,這三部分直接就是與JUnit的arrange-act-assert中的部分相對應的。在given塊裡的語句即是arrange 階段,而在when 塊裡的則對應于act階段,assert階段在Spock裡則由then代碼塊所實作。
相對于使用普通的注釋(就像Junit例子中所示那樣)來說,Spock的标簽有兩個比較大的優點:
1. Spock标簽是完整的英國文本,并且也同時會展示在測試報告中(之後還會有更多說明)
2. Spock标簽同時還會有語義值。
第二點最好是可以通過一個反面的例子來說明。
在JUnit測試中,arrange-act-assert 模式是隐含的。讓我們假設一個頑皮的Java開發人員修改JUnit測試成如下這般摸樣:
@Test
public void chargeCreditCard() {
CreditCardBilling billing = new CreditCardBilling();
Client client = new Client();
billing.charge(client,150);
assertEquals("Revenue should be recorded",150,billing.getCurrentRevenue());
Client client2 = new Client();
billing.charge(client2,100);
assertEquals("Revenue should be recorded",250,billing.getCurrentRevenue());
}
在這個簡單的測試中,還是可以很容易就了解到底發生了什麼。但是在更大的測試中,增加額外的斷言和設定語句會使得測試難于閱讀了解。Spock可以使得測試結構非常清晰。除了基本的given-when-then,Spock還有其他幾種代碼塊可以用來保證測試的結構比較清晰。
這個頑皮的開發人員,不會以類似的方式修改Spock的測試。相反的,一個更好的Spock測試如下所示:
public void "charging a credit card - two transactions"() {
given: "a billing service ready to accept payments"
CreditCardBilling billing = new CreditCardBilling();
and: "two customers with valid credit cards"
Client client1 = new Client();
Client client2 = new Client();
when: "first client buys something with 150 dollars"
billing.charge(client1,150);
then: "we expect the transaction to be recorded"
billing.getCurrentRevenue() == 150
when: "second client buys something with 100 dollars"
billing.charge(client2,100);
then: "we expect the transaction to be recorded"
billing.getCurrentRevenue() == 250
}
這裡我們使用了and 代碼塊來合并兩個arrange 階段。而when和then代碼塊使得該測試閱讀起來很自然流暢。僅僅通過閱讀代碼塊的名字(given-and-when-then-when-then),我們就可以很清楚的知道這個測試是做什麼的。
Spock在測試失敗時會提供更有用的資訊,這是Spock應用到我自己的項目裡的最牛逼的一個特性。
永遠不要相信一個你沒看到其失敗的測試。 – Colin Vipurs
單元測試的首要目的是發現回歸中的錯誤。經常會發生一個測試在之前的建構中通過而在目前的建構中卻失敗了的情況。
你想讓你的測試失敗,這看起來似乎違反直覺。會失敗的測試就是能工作的測試。
JUnit的測試出現失敗時總是需要調試(才能找到問題原因)。不幸的是,JUnit測試失敗了通常就需要進行調試,因為在失敗的情況下所給出的資訊是最基本的(不夠詳細)。
這裡有一個為示範需要故意實作的例子:
@Test
public void similarBooks() {
Book book = new Book("The Murder on the Links");
List<String> similar = book.findSimilarTitles();
assertEquals("Murder on the Orient Express",similar.get(0));
}
假設你的項目的最新的建構失敗了。你檢查了本地的代碼,運作了所有的測試,然後得到了如下結果:

隻是看這個失敗提示并沒什麼幫助。你知道是哪個測試失敗了,但是卻很難弄明白如何去修複這個失敗。
在這個例子中,你需要在調試器裡運作該測試才可以弄清楚為什麼你得到了一個跟你預期不一緻的值。
Spock 為您提供足夠的上下文資訊, 假定你使用Spock重寫了同樣的測試:
public void "find books with similar titles"() {
given: "A book that contains the word murder"
Book book = new Book("The Murder on the Links")
when: "we search similar books"
List<String> similar = book.findSimilarTitles()
then: "similar books should have murder in the title"
similar.get(0) == "Murder on the Orient Express"
}
這個測試當然會失敗,但是這次你會得到如下提示:

跟JUnit不同,Spock知道失敗的上下文資訊。在這個例子中,Spock為你展示了相似圖書的整個清單的内容。僅僅通過檢視這個失敗的測試你就能夢看到你要搜尋的圖書名确實是在清單中,但是是在第二個位置。這個測試希望該值位于第一位,是以測試就失敗了。
現在你就知道你的代碼失敗是由于一個索引的變化(或者是一段代碼将相似圖書的順序做了調整)。有了這些知識,你就可以更快的查明問題原因。
Spock測試可以被非技術人員所讀懂。
單元測試的命名在企業級應用裡很重要。開發人員通常認為由測試産生的報告中有技術性術語會有什麼不妥。
計算機科學領域有兩大難題:緩存失效和命名問題。– Phil Karlton
JUnit裡的方法名稱受到Java規範限制。一個典型的錯誤就是在單元測試裡使用不知道說些什麼的标題。下面是一個極其反面的例子:

這個命名“scheme”的問題是它對于手頭沒有代碼的人來說一點用都沒有。如果測試名為“scenario2”的方法因為某種原因失敗了,除了開發人員沒有其他人能夠明白它會造成如何影響。經驗豐富的Java開發者會試圖将他們的單元測試使用其真實的意圖來命名。這樣明顯會更好些,但是仍然不夠完美,因為它們由于Java語言的限制,隻能采用駝峰式命名(或者下劃線):

Spock 支援簡單的英語句子。
Spock測試的優美之處,在于其可以使用完整的英語句子描述來作為其方法名(這在前面部分的例子中已經有所展現了)。
Maven(和其他任何JUnit相關的工具)可以對它們進行格式化,且無需額外的配置:

然而Spock可以将這份報告做進一步改善。Spock擁有其自己的報告子產品,可以顯示包含在Spock的塊代碼(比given,when,then)的所有文本。結果如下:

這對于Spock測試的可讀性來說是個很大的改進。
非技術人員 也可以讀懂該報告,并且在無需知道Java是如何工作的前提下就能做出合适的抉擇。
測試者 可以通過閱讀Spock測試,來跟他們自己的測試用例進行對比。
業務分析人員 可以通過閱讀Spock測試,來驗證它們是否是按照系統規格要求來實作的。
項目管理者 可以通過閱讀Spock報告,進而清楚系統目前的狀态。
他們可以及時的發現一個失敗的測試會産生什麼樣(或高或低)的影響。
Spock對于參數化測試有一個自定義的DSL。
在JUnit裡,一個非常常見的反模式,是有相當一部分測試是99.9%的地方都是相同的,而隻有一些變量是不一樣的。
請看如下例子:
@Test
public void acceptJpg() {
ImageNameValidator validator = new ImageNameValidator();
String pictureFile = "scenery.jpg";
assertTrue(validator.isValidImageExtension(pictureFile));
}
@Test
public void acceptJpeg() {
ImageNameValidator validator = new ImageNameValidator();
String pictureFile = "house.jpeg";
assertTrue(validator.isValidImageExtension(pictureFile));
}
@Test
public void doNotAcceptTiff() {
ImageNameValidator validator = new ImageNameValidator();
String pictureFile = "sky.tiff";
assertFalse(validator.isValidImageExtension(pictureFile));
}
這些JUnit測試很顯然不符合DRY原則(Don't repeat yourself,不要重複自己),因為它們有太多的相同代碼了。
實際上它們都是相同的測試邏輯(傳遞一個圖檔給ImageValidator類),而且唯一變化的東西就是檔案名和期待的結果。
以下圖文可以更好的解釋這些測試的相似性:

這種類型的測試被叫做參數化的測試,因為它們擁有相同的測試邏輯,而且所有的場景均依賴于傳遞給該測試邏輯的不同的參數。
JUnit對于參數化的支援非常有限而且很受限制
使用JUnit進行參數化測試是可以完成的,但是由此導緻的文法卻是相當的醜陋。我建議你去閱讀下JUnit的官方文檔。 我會等着你回來的。:)
如果你從未見過JUnit參數化測試的步驟的話,不用擔心。這裡做個清晰的說明:
- 它需要一個自定義的運作期runner(@RunWith(Parameterized.class));
- 測試類必須添加用于辨別輸入的字段;
- 測試類必須添加用于辨別輸出的字段;
- 需要一個特殊的構造器來注入所有的輸入和輸出;
- 測試資料會被裝載到一個二維對象數組中(之後會被轉換為一個清單);
- 測試資料和測試描述是分開的;
- 你無法很容易的地在同一個類中使用兩個測試。
基本上來說,這個由JUnit提供的“解決方案”顯得非常麻煩而且吃力不讨好,這也是為什麼大量的Java開發人員并不知道JUnit參數化測試的存在。
我知道有很多外部的類庫可以增強實作JUnit參數化測試的方式,但是它們的存在進一步加強我的論點,
即Spock不像JUnit那樣,是自帶電池的(譯者注:這裡是指支援參數化測試的方式)。
Spock以一種直覺的方式編寫參數化測試。
Spock可以提供資料表格來使得單元測試更容易了解。
由于其使用了Groovy DSL,是以使得你可以講資料和其描述以表格的方式放在一起:
public void "Valid images are PNG and JPEG files"() {
given: "an image extension checker"
ImageNameValidator validator = new ImageNameValidator()
expect: "that only valid filenames are accepted"
validator.isValidImageExtension(pictureFile) == validPicture
where: "sample image names are"
pictureFile || validPicture
"scenery.jpg" || true
"house.jpeg" || true
"car.png" || true
"sky.tiff" || false
"dance_bunny.gif" || false
}
這裡你可以看到三個JUnit的測試被合并為一個單獨的Spock測試,這裡有幾個明顯的優點:
- 不存在代碼重複。測試邏輯隻需要編寫一次;
- 所有的輸入和輸出都集中在一個地方(即where代碼塊);
- 參數的名稱在表格的頭部清晰可見。 Spock資料表格的靈活性随着測試的增長會突顯其強大之處。 增加一個新的測試場景僅需要增加一行即可。 在上面的例子中,我增加了兩個場景,即png和gif的圖像格式,隻花了很少的代碼。
增加一個新的輸入或者輸出變量也是相當的容易,因為你隻需要給表格增加一列即可。
有趣的是,如果你運作單獨的這一個Spock測試,而且使用了Spock 的 Unroll 注解的話,實際上會運作多個測試(每一行都會運作一個測試)。你甚至可以讓每個測試運作單獨使用自定義字元串進行命名,這樣每個測試運作都可以描述它做了什麼:
@Unroll("Running image #pictureFile with result #validPicture")
public void "Valid images are PNG and JPEG files"() {
given: "an image extension checker"
ImageNameValidator validator = new ImageNameValidator()
expect: "that only valid filenames are accepted"
validator.isValidImageExtension(pictureFile) == validPicture
where: "sample image names are"
pictureFile || validPicture
"scenery.jpg" || true
"house.jpeg" || true
"car.png" || true
"sky.tiff" || false
"dance_bunny.gif" || false
}
這裡是運作結果:

這對于擁有大量數目的測試來說尤其有用。如果其中一個測試失敗了,你就可以在測試報告中看出哪個測試失敗了(而不是将整個測試都标記為失敗)。
Spock資料表格是參數化測試最基本的形式。Spock同樣支援資料管道和自定義疊代器,可以滿足對輸入輸出參數處理的更強大的需求。
在以後的文章裡我們将會探索Spock所提供的關于參數化測試的所有功能 - 因為它們值得我們對其單獨進行分析。
mocking能力
Spock擁有内置的mocking和stubbing功能。
當談到mocking時,JUnit真沒有可比性,因為JUnit甚至還不支援mocking。
當你需要在JUnit中用到mocking的時候,你就會需要引入獨立的架構來支援。
Java已經有幾個mocking架構了,但是最近占主導地位的是Mockito。
在另一方面,Spock則緻力于滿足你所有的測試需求,是以在基本包裡就内置了強大的mock和stubs的支援。
注意: 如果你還不知道什麼是mocking,或者從來沒使用過Mockito,那麼你首先就應該去閱讀下這篇關于Mockito的入門文章:
Stubbing and Mocking with Mockito 2 and JUnit。
使用Spock實作基本的Stubbing
Spock可以使用其自己更為簡單的文法實作典型的Mockito套路語句:
when(something).thenReturn(somethingElse) 。
Spock不會引入兩個新的方法(when 和 then),而是私用>>操作符,意思是“傳回那個東西”。
舉個例子吧,讓我們假定你在為一個準許貸款的銀行編寫單元測試。以下是核心邏輯
public class LoanApprover {
public boolean approveLoan(Customer customer, long amount){
if(amount < 1000){
return true;
}
if(amount < 50000 && customer.hasGoodCreditScore()){
return true;
}
return false;
}
}
我們來同時看看使用Mockito 和Spock所實作的相同單元測試分别是怎樣的:
//JUnit/Mockito Test method
@Test
public void goodCredit(){
Customer sampleCustomer = mock(Customer.class);
when(sampleCustomer.hasGoodCreditScore()).thenReturn(true);
LoanApprover loanApprover = new LoanApprover();
assertTrue(loanApprover.approveLoan(sampleCustomer, 10000));
}
//Spock Test method
public void "customer with good credit and loan of 10000 should be approved"() {
given: "a customer with good credit"
Customer sampleCustomer = Stub(Customer.class)
sampleCustomer.hasGoodCreditScore() >> true
expect: "an approval of the loan"
LoanApprover loanApprover = new LoanApprover()
loanApprover.approveLoan(sampleCustomer, 10000) == true
}
在這個簡單的例子中,你可以看到,Spock和Mockito工作的方式很相近。Spock裡的插入符文法将Mockito裡的when/thenReturn文法合二為一。
值得注意的是,與Mockito不同,Spock清楚知道虛假對象的本質(比如,是stub,還是mock)。在這個特定的例子中,我隻是查詢了Customer來擷取一個傳回結果,是以在Spock裡我們建立了一個Stub,而不是一個Mock。這裡差別是微小的,但是在接下來的部分就會顯得更加明顯了。
Mockito裡對于stubbing的各種你所喜歡的特性,在Spock裡同樣也是支援的。我不會在這裡繼續深入詳細闡述,但是像多次傳回值、比對特定的參數,或者是建立自定義的響應,這些在Spock裡都是很容易實作的(而且通常來說文法更簡單些)。
使用Spock實作基本的Mocking
我們假定LoanApprover 類更加聰明了些。
如果貸款被審批或者被拒後,它不會傳回一個布爾類型的結果,而是發了一封郵件。
以下是代碼:
public class LoanApproverWithEmail {
private final EmailService emailService;
public LoanApproverWithEmail(final EmailService emailService){
this.emailService = emailService;
}
public void approveLoan(Customer customer, long amount){
if(loanApproved(customer, amount)){
emailService.sendConfirmation(customer.getEmailAddress());
}
else{
emailService.sendRejection(customer.getEmailAddress());
}
}
private boolean loanApproved(Customer customer, long amount){
if(amount < 1000){
return true;
}
if(amount < 50000 && customer.hasGoodCreditScore()){
return true;
}
return false;
}
}
這一次,我們利用構造注入來使用一個外部的電子郵件服務,為了達到我們的目的,它有兩個方法,分别叫sendConfirmation()和sendRejection()。
由于我們的測試方法 -approveLoan - 是帶有void傳回值的(既無傳回值),我們這裡就不能使用stub來編寫單元測試了。我們需要mock 這個EmailService,并且檢查它在單元測試完成之後做了些什麼。
編寫一個Mockito 測試很簡單(假定已經很熟悉Mockito)。我們需要使用Mockito的 verify 指令,而不是JUnit的斷言。
@Test
public void lowAmountIsAlwaysAccepted(){
Customer sampleCustomer = new Customer();
EmailService emailService = mock(EmailService.class);
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
//Loans that low will be accepted regardless of credit score
loanApprover.approveLoan(sampleCustomer, 600);
verify(emailService).sendConfirmation(sampleCustomer.getEmailAddress());
verify(emailService,times(0)).
sendRejection(sampleCustomer.getEmailAddress());
}
@Test
public void bigAmountsAreAlwaysRejected(){
Customer sampleCustomer = new Customer();
EmailService emailService = mock(EmailService.class);
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
//Loans that high will be rejected regardless of credit score
loanApprover.approveLoan(sampleCustomer, 75000);
verify(emailService,times(0)).
sendConfirmation(sampleCustomer.getEmailAddress());
verify(emailService).sendRejection(sampleCustomer.getEmailAddress());
}
這兩個單元測試mock了EmailService 類。是以一旦貸款被請求,我們就會檢查發送到客戶那裡的郵件的類型。如果是發送的是一封确認信,那麼我們就知道貸款被審批通過了。如果發送的是拒絕信,我們就知道代碼沒有被審批通過。
為了讓單元測試更加嚴謹些,我們還需要驗證确實隻給客戶發送了一種類型的郵件。對于客戶來說,如果對于同一次貸款申請同時收到了拒絕信和确認信,那将會是很尴尬的事情。
在這種特定情況下,對于電子郵件服務的mocking就至關重要了,是以我們也要同時要避免每次測試運作時都發送一封真實郵件的情況發生。
現在我們看看使用Spock如何實作同樣的測試:
public void "very low loan amounts are always rejected"() {
given: "a customer with any credit"
Customer sampleCustomer = new Customer()
and: "an email service that is mocked"
EmailService emailService = Mock(EmailService.class)
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
when: "customer requests a loan lower than 1000 USD"
loanApprover.approveLoan(sampleCustomer, 600);
then: "a confirmation email is sent to the customer"
1 * emailService.sendConfirmation(sampleCustomer.getEmailAddress())
0 * emailService.sendRejection(sampleCustomer.getEmailAddress())
}
public void "very high loan amounts are always rejected"() {
given: "a customer with any credit"
Customer sampleCustomer = new Customer()
and: "an email service that is mocked"
EmailService emailService = Mock(EmailService.class)
LoanApproverWithEmail loanApprover =
new LoanApproverWithEmail(emailService);
when: "customer requests a loan higher than 50000 USD"
loanApprover.approveLoan(sampleCustomer, 75000);
then: "a rejection email is sent to the customer"
0 * emailService.sendConfirmation(sampleCustomer.getEmailAddress())
1 * emailService.sendRejection(sampleCustomer.getEmailAddress())
}
與Mockito類似,沒有使用JUnit斷言來實作。
相反的,使用了一種特殊的Spock文法來進行方法驗證。其格式如下:
N * mockedObject.method(arguments)
這一行意思是:“在測試結束之後,mockedObject對象的method 方法使用參數arguments時應該隻被執行過N次”。如果事實确實如此發生,那麼測試就會通過。否則Spock就會将測試标記為失敗。
這種文法比Mockito更加幹淨些,因為你無須指定verify 和times指令。這樣的驗證代碼更接近于真正的Java代碼。
同時要注意,被模拟的對象這次是通過Mock()來建立的,而不像前面的例子裡通過Stub()建立。
Spock 使讀者能夠厘清楚哪些類是用來檢測模拟結果的(stubs),而哪些又是用于驗證的(mocks),然而Mockito卻沒有做這種區分。
從技術上講,測試在這兩種情況下工作是一樣的,但是考慮到可讀性的話,Spock的方式明顯要更好一些,特别是對于那些有很多虛假對象要建立的大型的單元測試來說。
Spock 比對器(以及為什麼它們比Mockito要好些)
假定我們的電子郵件服務在郵件發送時會記錄下其時間。發送郵件的兩個方法都增加了這個參數。
public interface EmailService {
void sendConfirmation(String emailAddress, LocalDateTime when);
void sendRejection(String emailAddress, LocalDateTime when);
}
我們同時假定該日期事先是不知道的。也許它是目前日期,也許是下一個工作日,也許是周末,我們并不關心。但是我們得模拟它。Mockito提供了幾個比對器(matchers)用來忽略參數的實際值。不幸的是,你可能已經知道了,Mockito并不支援使用真實參數的混合比對器。我們先用Mockito來實作一下:
(如果你是Mockito的老手,你就會知道這個測試根本就無法運作。)

這裡Mockito清楚地告訴我們我們需要對所有的參數都要使用比對器。
為了克服這種限制,我們修改單元測試,并使用anyString比對器忽略掉了email方法的第一個參數。
@Test
public void lowAmountIsAlwaysAccepted(){
Customer sampleCustomer = new Customer();
EmailService emailService = mock(EmailService.class);
LoanApproverWithDate loanApprover = new LoanApproverWithDate(emailService);
//Loans that high will be rejected regardless of credit score
loanApprover.approveLoan(sampleCustomer, 50000);
verify(emailService,times(0)).
sendConfirmation(anyString(), any());
verify(emailService).
sendRejection(anyString(), any());
}
現在測試可以正确運作了。然而這并不是我們想要的那樣嚴格。因為電子郵件位址現在也被忽略掉了,我們也就無法确定電子郵件位址是正确的并且真實的反映了使用者的電子郵件位址。在這個認為編造出來的例子中,看起來确實不像什麼大問題,但是在真實的單元測試裡,這個Mockito的限制卻有可能将bug帶入到生産環境中。
跟Mockito一樣,Spock支援忽略方法參數并使用下劃線字元 _ 來辨別它們。然而與Mockito 不同的是, 它卻是支援比對器與真實參數混合出現的情形。是以我們原來的忽略日期并檢查郵件的測試在Spock裡就是可以直接支援的。
這個測試正确運作,因為Spock确實支援比對器與真實參數同時出現。
<span class="lake-selected" data-card-type="inline" data-lake-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fintranetproxy.alipay.com%2Fskylark%2Flark%2F0%2F2019%2Fpng%2F7923%2F1575356856999-96b47da6-5ffc-4727-b52d-5dcec2a4abd9.png%22%2C%22originWidth%22%3A769%2C%22originHeight%22%3A163%2C%22name%22%3A%22image.png%22%2C%22size%22%3A19306%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22ocrLocations%22%3A%5B%7B%22x%22%3A-0.8010417%2C%22y%22%3A24.031252%2C%22width%22%3A151.39688170000002%2C%22height%22%3A13.617708000000004%2C%22text%22%3A%22Finishedafter0.964seconds%22%7D%2C%7B%22x%22%3A142.58542%2C%22y%22%3A51.26667%2C%22width%22%3A55.2718800