Spock測試套件
Spock套件基於一個單元測試框架,它有比junit更為簡潔高效的測試語法。
核心概念
整體認識
Spock中一個單元測試類名叫Specification。所有的單元測試類,都需要繼承Specification
class MyFirstSpecification extends Specification {
// fields
// fixture methods
// feature methods
// helper methods
}
對於spock來說,Specification代表了一個軟體、應用、類的使用規範,其中的所有單元測試方法,被稱為feature,即功能。
一個feature method的執行邏輯大概是如下幾步:
- setup 設定該功能的前置配置
- stimulus 提供一個輸入,觸發該功能
- response 描述你期望該功能的返回值
- cleanup 清理功能的前置配置
所以,對spock來說,一個單元測試,其實是這個軟體應用提供的功能使用規範,這個規範中提供了每個功能的使用說明書,輸入什麼,會得到什麼,大體是按這個看法,去寫單元測試的。
前置、後置
就像junit一樣,我們可以對整個單元測試類做一些前置,並清理。也可以對每個單元測試的方法做一些前置後清理。
其跟Junit的類比關係為
setupSpec 對應 @BeforeClass
setup 對應 @Before
cleanup 對應 @After
cleanupSpec 對應 @AfterClass
同時由於Spock的單元測試本身是會整合Specification 父類的,所以父類中的前置、後置方法也會被呼叫,不過不用顯示呼叫,會自動呼叫。
一個測試功能方法執行時,其整體的執行順序為:
super.setupSpec
sub.setupSpec
super.setup
sub.setup
**feature method
sub.cleanup
super.cleanup
sub.cleanupSpec
super.cleanupSpec
同junit的類比
Feature 方法
blocks
feature的具體寫法有很多的block組成,這些block對應的feature方法本身的四個階段(setup, stimulus, reponse, cleanup) 。每個block對應階段示意圖
典型的用法
def '測試++'(){
given:
def x = 5
when: def result = calculateService.plusPlus(x)
then: result == 6
}
- given也可以寫成setup,feature方法裡的given其實跟外面的setup方法功能一樣,都是做測試功能的前置設定。只是單獨的setup方法,是用來寫對每個測試feature都有用的測試。只跟當前feature相關的設定,請放在feature方法內的given標籤
- when 標籤用來實際呼叫想要測試的feature
- then 中對when的呼叫返回進行結果驗證,這裡不需要寫斷言,直接寫表示式就是斷言
異常condition
then中的斷言在spock中叫condition。比如Java中的Stack在沒有元素時,進行Popup,則會EmptyStackException異常。我們期望它確實會丟擲這個異常,那麼寫法如下
def '異常2'() {
given:
def stack = new Stack()
when:
def result = stack.pop()
then:
EmptyStackException e = thrown()
}
它並不會丟擲EmptyStackException,我們要測試這個預期的話,程式碼如下:
def '異常2'() {
given:
def stack = new Stack()
stack.push("hello world")
when:
stack.pop()
then:
EmptyStackException e = notThrown()
}
then和expect的區別
前面說了when block用來呼叫,then用來判斷預期結果。但有的時候,我們的呼叫和預期判斷並不複雜,那麼可以用expect將兩者合在一起,比如以下兩段程式碼等價
when:
def x = Math.max(1, 2)
then:
x == 2
expect:
Math.max(1, 2) == 2
cleanup block的用法
def 'cleanup'() {
given:
def file = new File("/some/path")
file.createNewFile()
// ...
cleanup:
file.delete()
}
用於清理feature測試執行後的一些設定,比如開啟的檔案連結。該操作即便測試的feature出異常,依然會被呼叫
同樣,如果多個測試feature都需要這個cleanup.那麼建議將cleanup的資源提到setup方法中,並在cleanup方法中去清理
測試用例中的文字描述
為了讓單元測試可讀性更高,可以將測試方法中每一部分用文字進行描述,多個描述可以用and來串聯
def '異常2'() {
given:'設定stack物件'
def stack = new Stack()
and:'其它變數設施'
stack.push('hello world')
when:'從stack中彈出元素'
def result = stack.pop()
then:'預期會出現的異常'
EmptyStackException e = thrown()
}
Extension
spock通過標註來擴充單元測試的功能
@Timeout
指定一個測試方法,或一個設定方法最長可以執行的時間,用於對效能有要求的測試
@Ignore
用於忽略當前的測試方法
@IgnoreRest
忽略除當前方法外的所有方法,用於想快速的測一個方法
@FailsWith
跟exception condition類似
資料驅動測試
資料表
對於有些功能邏輯,其程式碼是一樣的,只是需要測試不同輸入值。按照先前的介紹,最簡潔的寫法為:
def "maximum of two numbers1"() {
expect:
// exercise math method for a few different inputs
Math.max(1, 3) == 3
Math.max(7, 4) == 4
Math.max(0, 0) == 1
}
缺點:
- Math.max程式碼需要手動呼叫三次
- 第二行出錯後,第三行不會被執行
- 資料和程式碼耦合在一起,不方便資料從其它地方獨立準備
所以spock引入了資料表的概念,將測試資料和程式碼分開。典型例項如下:
class MathSpec extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
1 | 3 || 3
7 | 4 || 7
0 | 0 || 0
}
}
- where語句中,定義資料表。第一行是表頭,定義這一列所屬的變數。
- 實際程式碼呼叫,只需要呼叫一次。程式碼中的變數跟資料表中的變數必須一一對應
- 看似一個方法,實際上執行時,spock會根據資料表中的行數,迴圈迭代執行程式碼。每一行都是獨立於其餘行執行,所以有setup和cleanup塊,對每一個行的都會重複執行一次
- 並且某一行的資料出錯,並不影響其餘行的執行
另外的寫法
def "maximum of two numbers"(int a, int b ,int c) {
expect:
Math.max(a, b) == c
where:
a | b | c
1 | 3 | 3
7 | 4 | 4
0 | 0 | 1
}
- 變數可以在方法引數中宣告,但沒必要
- 資料表可以全部用一個豎線來分割,但無法像兩個豎線一樣清晰的分割輸入和輸出
更清晰的測試結果展示
class MathSpec extends Specification {
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
a | b || c
1 | 3 || 3
7 | 4 || 4
0 | 0 || 1
}
}
以上測試程式碼,資料表中的後兩行會執行失敗。但從測試結果皮膚中,不能很好的看到詳細結果
使用@Unroll
可以將每個迭代的執行結果輸出
可以看到皮膚中實際輸出的文字為測試方法的名稱。如果像在輸出中加上輸入輸出的變數,來詳細展示每個迭代,可以在方法名中使用佔位符#variable
來引用變數的值。舉例如下:
@Unroll
def "maximum of #a and #b is #c"() {
expect:
Math.max(a, b) == c
where:
a | b || c
1 | 3 || 3
7 | 4 || 4
0 | 0 || 1
}
更豐富的資料準備方式
前面的資料表顯示的將資料以表格的形式寫出來。實際上,資料在where block中的準備還有其它多種方式。
where:
a << [1, 7, 0]
b << [3, 4, 0]
c << [3, 7, 0]
從資料庫中查詢
@Shared sql = Sql.newInstance("jdbc:h2:mem:", "org.h2.Driver")
def "maximum of two numbers"() {
expect:
Math.max(a, b) == c
where:
[a, b, c] << sql.rows("select a, b, c from maxdata")
}
使用groovy程式碼賦值
where:
a = 3
b = Math.random() * 100
c = a > b ? a : b
以上幾種方式可以混搭。
其中方法名也可以以豐富的表示式引用where block中的變數
def "person is #person.age years old"() {
...
where:
person << [new Person(age: 14, name: 'Phil Cole')]
lastName = person.name.split(' ')[1]
}
基於互動的測試(Interaction Based Testing)
有的時候,我們測試的功能,需要依賴另外的collaborators來測試。這種涉及到多個執行單元之間的互動,叫做互動測試
比如:
class Publisher {
List<Subscriber> subscribers = []
int messageCount = 0
void send(String message){
subscribers*.receive(message)
messageCount++
}
}
interface Subscriber {
void receive(String message)
}
我們想測Publisher,但Publisher有個功能是是發訊息給所有的Subscriber。要想測試Publisher的傳送功能確實ok,那麼需要測試Subscriber的確能收到訊息。
使用一個實際的Subscriber實現固然能實現這個測試。但對具體的Subscriber實現造成了依賴,這裡需要Mock。使用spock的測試用例如下:
class PublisherTest extends Specification{
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
Subscriber subscriber2 = Mock() //建立依賴的Subscriber Mock
def setup() {
publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
publisher.subscribers << subscriber2
}
def "should send messages to all subscribers"() {
when:
publisher.send("hello") //呼叫publisher的方法
then:
1*subscriber.receive("hello") //期望subscriber的receive方法能被呼叫一次
1*subscriber2.receive("hello")//期望subscriber1的receive方法能被呼叫一次
}
}
以上程式碼的目的是通過mock來測試當Publisher的send的方法被執行時,且執行引數是'hello'時,subscriber的receive方法一定能被呼叫,且入參也為‘hello’
對依賴Mock的呼叫期望,其結構如下
1 * subscriber.receive("hello")
| | | |
| | | argument constraint
| | method constraint
| target constraint
cardinality
cardinality
定義右邊期望方法執行的次數,這裡是期望執行一次,可能的寫法有如下:
1 * subscriber.receive("hello") // exactly one call
0 * subscriber.receive("hello") // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello") // any number of calls, including zero
target constraint
定義被依賴的物件。可能的寫法如下
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello") // a call to any mock object
Method Constraint
定義在上述物件上期望被呼叫的方法,可能的寫法如下:
1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello") // a method whose name matches the given regular expression
// (here: method name starts with 'r' and ends in 'e')
Argument Constraints
對被呼叫方法,期望的入參進行定義。可能寫法如下:
1 * subscriber.receive("hello") // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello") // an argument that is unequal to the String "hello"
1 * subscriber.receive() // the empty argument list (would never match in our example)
1 * subscriber.receive(_) // any single argument (including null)
1 * subscriber.receive(*_) // any argument list (including the empty argument list)
1 * subscriber.receive(!null) // any non-null argument
1 * subscriber.receive(_ as String) // any non-null argument that is-a String
1 * subscriber.receive(endsWith("lo")) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 && it.contains('a') })
// an argument that satisfies the given predicate, meaning that
// code argument constraints need to return true of false
// depending on whether they match or not
// (here: message length is greater than 3 and contains the character a)
一些萬用字元
1 * subscriber._(*_) // any method on subscriber, with any argument list
1 * subscriber._ // shortcut for and preferred over the above
1 * _._ // any method call on any mock object
1 * _ // shortcut for and preferred over the above
嚴格模式(Strict Mocking)
when:
publisher.publish("hello")
then:
1 * subscriber.receive("hello") // demand one 'receive' call on 'subscriber'
_ * auditing._ // allow any interaction with 'auditing'
0 * _ // don't allow any other interaction
預設情況下,你對Mock例項的方法的呼叫,會返回該方法返回值的預設值,比如該方法返回的是布林型,那麼你你呼叫mock例項中的該方法時,將返回布林型的預設值false.
如果我們希望嚴格的限定Mock例項的各方法行為,可以通過上述程式碼,對需要測試的方法顯示定義期望呼叫行為,對其它方法設定期望一次都不呼叫。以上then block中的0 * _
即是定義這種期望。當除subscriber中的receive和auditing中的所有方法被呼叫時,該單元測試會失敗,因為這不符合我們對其它方法呼叫0次的期望
呼叫順序
then:
2 * subscriber.receive("hello")
1 * subscriber.receive("goodbye")
以上兩個期望被呼叫的順序是隨機的。如果要保證呼叫順序,使用兩個then
then:
2 * subscriber.receive("hello")
then:
1 * subscriber.receive("goodbye")
Stubbing 定義方法返回
前面的interaction mock是用來測試被mock的物件,期望方法的呼叫行為。比如入參,呼叫次數。
而stubbing則用來定義被mock的例項,在呼叫時返回的行為
總結,前者定義呼叫行為期望,後者定義返回行為期望。且Interaction test 測試的是執行期望或斷言。的stubbing則是用來定義mock的模擬的行為。
所以stubbing 對mock方法返回值的定義應該放在given block. 而對mock方法本身的呼叫Interaction test 應該放在then block中。所以stubbing對返回值的定義相當於在定義測試的測試資料。
Stubbing的使用場景也很明確。假設Publisher需要依賴Subscriber方法的返回值,再做下一步操作。那我們就需要對Subscriber的返回值進行mock,來測試不同返回值對目標測試程式碼(feature)的行為。
我們將上述Subscriber介面對應的方法新增一個返回值
class Publisher {
Subscriber subscriber
int messageCount = 0
int send(String message){
if(subscriber.receive(message) == 'ok') {
this.messageCount++
}
return messageCount
}
}
interface Subscriber {
String receive(String message)
}
測試程式碼舉例
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
def setup() {
publisher.subscriber = subscriber
}
def "should send msg to subscriber"() {
given:
subscriber.receive("message1") >> "ok"
when:
def result = publisher.send("message1")
then:
result == 1
}
以上程式碼表示,模擬subscriber.receive被呼叫時,且呼叫引數為message1,方法返回ok. 而此時期望(斷言)Publisher的send方法,返回的是1
stubbing 返回值結構
subscriber.receive(_) >> "ok"
| | | |
| | | response generator
| | argument constraint
| method constraint
target constraint
注意這裡多了response generator,並且沒有interaction test中的Cardinality
各種返回值定義
返回固定值
subscriber.receive("message1") >> "ok"
subscriber.receive("message2") >> "fail"
順序呼叫返回不同的值
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
第一次呼叫返回ok,第二次、三次呼叫返回error。剩下的呼叫返回ok
根據入參計算返回值
subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
上述兩者效果都一樣,都是對第一個入參的長度進行判斷,然後確定返回值
返回異常
subscriber.receive(_) >> { throw new InternalError("ouch") }
鏈式返回值設定
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
前三次呼叫依次返回ok,fail,ok。第四次呼叫返回異常,之後的呼叫返回ok
將Interaction Mock和stubbing組合
1 * subscriber.receive("message1") >> "ok"
1 * subscriber.receive("message2") >> "fail"
這裡即定義了被mock 的subscriber其方法返回值,也定義了該方法期望被呼叫多少次。舉例:
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
def setup() {
publisher.subscriber = subscriber
}
def "should send msg to subscriber"() {
given:
1*subscriber.receive("message1") >> "ok"
when:
def result = publisher.send("message1")
then:
result == 1
}
以上寫法,即測試了subscriber.receive被呼叫了一次,也測試了publisher.send執行結果為1.如果將Interaction Mock和stubbing組合拆開,像下面這種寫法是不行的:
Publisher publisher = new Publisher()
Subscriber subscriber = Mock()
def setup() {
publisher.subscriber = subscriber
}
def "should send msg to subscriber"() {
given:
subscriber.receive("message1") >> "ok"
when:
def result = publisher.send("message1")
then:
result == 1
1*subscriber.receive("message1")
}
如何建立單元測試類
方式一
像Junit一樣,在需要測試的類上,使用Idea的幫助快捷鍵,然後彈出
選擇指定的測試框架spock和路徑即可
方式二
直接在指定的測試目錄下,新建對應的測試類,注意是新建groovy class
在Idea中,groovy class的圖示是方塊,java class是圓形,注意區分
有可能建完後,對應的圖示是
,說明Ide沒有識別到這是個groovy 類,一般是由於其程式碼有問題,可以開啟該檔案,把具體的錯誤修復,比如把註釋去掉之類的
參考資料
http://spockframework.org/spock/docs/1.1/all_in_one.html#_introduction