運行 Spock 測試時捕獲方法參數
一、簡介
當我們測試程式碼時,我們有時會想要捕獲傳遞給我們方法的參數。
在本教程中,我們將學習如何使用Stubs, Mock
和Spie
在 Spock 測試中捕獲參數,並檢查捕獲的內容。我們還將學習如何使用不同的參數驗證對同一Mock
多次調用,並斷言這些調用的順序。
2. 我們測試的主題
首先,我們需要一個接受我們想要捕獲的單一參數或參數的方法。
因此,讓我們使用catchMeIfYouCan()
方法建立一個ArgumentCaptureSubject
,該方法接受一個String
並傳回它,並在前面加上「Received」:
public class ArgumentCaptureSubject {
public String catchMeIfYouCan(String input) {
return "Received " + input;
}
}
3. 準備我們的數據驅動測試
我們將從Stub
的典型使用開始我們的測試,並改進它以捕獲參數。
讓我們建立一個類別的Stub
以傳回「42」的存根回應並呼叫其catchMeIfYouCan()
方法:
def "given a Stub when we invoke it then we capture the stubbed response"() {
given: "an input and a result"
def input = "Input"
def stubbedResponse = "42"
and: "a Stub for our response"
@Subject
ArgumentCaptureSubject stubClass = Stub()
stubClass.catchMeIfYouCan(_) >> stubbedResponse
when: "we invoke our Stub's method"
def result = stubClass.catchMeIfYouCan(input)
then: "we get our stubbed response"
result == stubbedResponse
}
我們在本範例中使用了一個簡單的Stub
,因為我們沒有驗證任何方法呼叫。
4. 捕捉論點
現在我們有了基本測試,讓我們看看如何捕獲用於呼叫方法的參數。
首先,我們將宣告一個方法範圍的變量,以便在捕獲參數時進行分配:
def captured
接下來,我們將用 Groovy Closure
取代靜態stubbedResponse
。當我們的Closure
方法被呼叫時,Spock 將向我們的閉包傳遞一個方法參數List
。
讓我們創建一個簡單的Closure
來捕獲arguments
列表並將其分配給我們captured
變數:
{ arguments -> captured = arguments }
對於我們的斷言,我們將斷言捕獲參數清單中索引 0 處的第一個元素等於我們的輸入:
captured[0] == input
因此,讓我們用captured
變數聲明更新我們的測試,用我們的參數捕獲Closure
替換我們的stubbedResponse
,並添加我們的斷言:
def "given a Stub when we invoke it then we capture the argument"() {
given: "an input"
def input = "Input"
and: "a variable and a Stub with a Closure to capture our arguments"
def captured
@Subject
ArgumentCaptureSubject stubClass = Stub()
stubClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments }
when: "we invoke our method"
stubClass.catchMeIfYouCan(input)
then: "we captured the method argument"
captured[0] == input
}
當我們想要返回一個stubbedResponse
並捕獲參數時,我們更新Closure
以返回它:
{ arguments -> captured = arguments; return stubbedResponse }
...
then: "what we captured matches the input and we got our stubbed response"
captured == input
result == stubbedResponse
請注意,雖然為了清楚起見我們使用了“ return
”,但這並不是絕對必要的,因為 Groovy 閉包預設會傳回最後執行的語句的結果。
當我們只對捕獲其中一個參數感興趣時,我們可以透過使用Closure
中的索引來捕獲我們想要的參數:
{ arguments -> captured = arguments[0] }
...
then: "what we captured matches the input"
captured == input
在這種情況下,我們captured
變數將與我們的參數具有相同的類型— String.
5. 與間諜一起抓捕
當我們想要捕獲一個值但又希望該方法繼續執行時,我們會加入Spy
的callRealMethod()
的呼叫。
讓我們更新我們的測試以使用Spy
而不是Stub
,並在Closure
中使用Spy
的callRealMethod()
:
def "given a Spy when we invoke it then we capture the argument and then delegate to the real method"() {
given: "an input string"
def input = "Input"
and: "a variable and a Spy with a Closure to capture the first argument and call the underlying method"
def captured
@Subject
ArgumentCaptureSubject spyClass = Spy()
spyClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments[0]; callRealMethod() }
when: "we invoke our method"
def result = spyClass.catchMeIfYouCan(input)
then: "what we captured matches the input and our result comes from the real method"
captured == input
result == "Received Input"
}
在這裡,我們捕獲了輸入參數,而不影響方法的返回值。
當我們想要在將捕獲的參數傳遞給真正的方法之前更改它時,我們會在閉包內更新它,然後使用Spy's callRealMethodWithArgs
來傳遞更新的參數。
因此,讓我們更新我們的Closure
,在將其傳遞給真正的方法之前將“Tampered:”添加到我們的String
中:
spyClass.catchMeIfYouCan(_) >> { arguments -> captured = arguments[0]; callRealMethodWithArgs('Tampered:' + captured) }
讓我們更新我們的斷言以期望我們的篡改結果:
result == "Received Tampered:Input"
6. 使用注入的模擬捕捉參數
現在我們已經了解如何使用 Spock 的模擬框架來捕獲參數,接下來讓我們將該技術應用於具有可模擬依賴項的類別。
首先,讓我們建立一個ArgumentCaptureDependency
類,我們的主題可以使用一個簡單的catchMe()
方法來呼叫該類,該方法接受並修改String
:
public class ArgumentCaptureDependency {
public String catchMe(String input) {
return "***" + input + "***";
}
}
現在,讓我們使用採用 ArgumentCaptureDependency 的建構子來更新ArgumentCaptureSubject
ArgumentCaptureDependency.
我們也加入一個不帶參數的callOtherClass
方法,並使用參數呼叫ArgumentCaptureDependency
的catchMe()
方法:
public class ArgumentCaptureSubject {
ArgumentCaptureDependency calledClass;
public ArgumentCaptureSubject(ArgumentCaptureDependency calledClass) {
this.calledClass = calledClass;
}
public String callOtherClass() {
return calledClass.catchMe("Internal Parameter");
}
}
最後,讓我們像以前一樣建立一個測試。這次,我們在建立ArgumentCaptureSubject
時將Spy
注入到它中,以便我們也可以callRealMethod()
並比較結果:
def "given an internal method call when we invoke our subject then we capture the internal argument and return the result of the real method"() {
given: "a mock and a variable for our captured argument"
ArgumentCaptureDependency spyClass = Spy()
def captured
spyClass.catchMe(_) >> { arguments -> captured = arguments[0]; callRealMethod() }
and: "our subject with an injected Spy"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(spyClass)
when: "we invoke our method"
def result = argumentCaptureSubject.callOtherClass(input)
then: "what we captured matches the internal method argument"
captured == "Internal Parameter"
result == "***Internal Parameter***"
}
我們的測試捕捉了內部參數“ Internal Parameter
”。此外,我們對Spy
的callRealMethod
的呼叫確保我們不會影響該方法的結果: “***Internal Parameter***”.
當我們不需要返回真實結果時,我們可以簡單地使用Stub
或Mock
。
請注意,當我們測試 Spring 應用程式時,我們可以使用 Spock 的SpringBean
註解來注入Mock
。
7. 從多次呼叫中捕獲參數
有時,我們的程式碼多次呼叫一個方法,並且我們希望捕獲每次呼叫的值。
因此,讓我們為 ArgumentCaptureSubject 中的callOtherClass()
方法新增一個String
參數ArgumentCaptureSubject.
我們將使用不同的參數調用它並捕獲它們。
public String callOtherClass(String input) {
return calledClass.catchMe(input);
}
我們需要一個集合來捕捉每次呼叫的參數。因此,我們將capturedStrings
變數宣告為ArrayList
:
def capturedStrings = new ArrayList()
現在,讓我們建立測試並使其呼叫callOtherClass()
兩次,第一次使用「First」作為參數,然後使用「Second」:
def "given an dynamic Mock when we invoke our subject then we capture the argument for each invocation"() {
given: "a variable for our captured arguments and a mock to capture them"
def capturedStrings = new ArrayList()
ArgumentCaptureDependency mockClass = Mock()
and: "our subject"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(mockClass)
when: "we invoke our method"
argumentCaptureSubject.callOtherClass("First")
argumentCaptureSubject.callOtherClass("Second")
}
現在,讓我們為模擬添加一個Closure
,以捕獲每次調用的參數並將其添加到我們的列表中。我們也可以透過在Mock
驗證我們的方法是否被呼叫了兩次:
then: "our method was called twice and captured the argument"
2 * mockClass.catchMe(_ as String) >> { arguments -> capturedStrings.add(arguments[0]) }
最後,我們斷言我們以正確的順序捕獲了兩個參數:
and: "we captured the list and it contains an entry for both of our input values"
capturedStrings[0] == "First"
capturedStrings[1] == "Second"
當我們不關心順序時,我們可以使用List
的contains
方法:
capturedStrings.contains("First")
8.使用多個Then
塊
有時,我們想要使用具有不同參數的相同方法來斷言一系列調用,但不需要捕獲它們。 Spock 允許在同一個then
區塊中以任意順序進行驗證,因此我們以什麼順序寫入它們並不重要。但是,我們可以透過新增多個then
區塊來強制執行順序。
Spock 驗證一個then
區塊中的斷言是否在下一個then
區塊中的斷言之前得到滿足。
因此,讓我們新增兩個then
區塊來驗證是否以正確的順序使用正確的參數呼叫我們的方法:
def "given a Mock when we invoke our subject twice then our Mock verifies the sequence"() {
given: "a mock"
ArgumentCaptureDependency mockClass = Mock()
and: "our subject"
@Subject argumentCaptureSubject = new ArgumentCaptureSubject(mockClass)
when: "we invoke our method"
argumentCaptureSubject.callOtherClass("First")
argumentCaptureSubject.callOtherClass("Second")
then: "we invoked our Mock with 'First' the first time"
1 * mockClass.catchMe( "First")
then: "we invoked our Mock with 'Second' the next time"
1 * mockClass.catchMe( "Second")
}
當我們的呼叫以錯誤的順序發生時,例如當我們先呼叫callOtherClass(“Second”)
時,Spock 會給我們一個有用的訊息:
Wrong invocation order for:
1 * mockClass.catchMe( "First") (1 invocation)
Last invocation: mockClass.catchMe('First')
Previous invocation:
mockClass.catchMe('Second')
9. 結論
在本教學中,我們學習如何使用 Spock 的Stub
、 Mock
和使用Closure
的Spies
擷取方法參數。接下來,我們學習如何在呼叫真正的方法之前使用Spy
更改捕獲的參數。我們也學習瞭如何在多次呼叫我們的方法時收集參數。最後,作為捕獲參數的替代方法,我們學習如何使用多個then
區塊來檢查我們的呼叫是否按正確的順序發生。
與往常一樣,本文的來源可以在 GitHub 上找到。