Spock測試框架
介紹
Spock是一個為groovy和java語言應用程式來測試和規範的框架。這個框架的突出點在於它美妙和高效表達規範的語言。得益於JUnit runner,Spock能夠在大多數IDE、編譯工具、持續整合服務下工作。Spock的靈感源於JUnit,jMock, RSpec, Groovy, Scala, Vulcans以及其他優秀的框架形態。
基本概念描述
Spock要求具備基本的groovy和單元測試知識。如果是java程式設計師,則不需要對groovy感到陌生,因為會java基本就會使用groovy了。
Spock測試類的一般結構
首先看一下測試模板方法的定義和JUnit的對比:
Spock基於Groovy,所以像寫java測試類一樣,需要先建立一個groovy檔案。
在一個groovy檔案裡面的第一行:import spock.lang.*,同時這個類需要繼承Specification。
舉例:
class MyFirstSpecification extends Specification { // 變數欄位 // 測試模板方法 // 測試方法 // 測試幫助方法 }
Spock的模板方法說明:
def setupSpec() {}// runs once -before the first feature method def setup() {}// runs before every feature method def cleanup() {}// runs after every feature method def cleanupSpec() {}// runs once -after the last feature method
定義變數:
def obj = new ClassUnderSpecification() def coll = new Collaborator()
定義測試方法:
def "pushing an element on the stack"() { // 測試程式碼塊,最重要的地方 }
程式碼塊一般結構,def定義的方法內容裡面:
given: def stack = new Stack() def elem = "push me" when:// 執行需要測試的程式碼 then:// 驗證執行結果
條件判斷:
when: stack.push(elem) then: !stack.empty stack.size() == 1 stack.peek() == elem
條件不滿足的情況輸出結果舉例:
Condition not satisfied: stack.size() == 2 ||| |1false [push me]
驗證丟擲異常舉例:
when: stack.pop() then: thrown(EmptyStackException) stack.empty
訪問異常內容:
when: stack.pop() then: def e = thrown(EmptyStackException) e.cause == null
驗證不會丟擲異常舉例:
//hashmap允許null的key def "HashMap accepts null key"() { given: def map = new HashMap() when: map.put(null, "elem") then: notThrown(NullPointerException) }
基於測試的互動,通過mock構造依賴的外部物件
def "events are published to all subscribers"() { given: def subscriber1 = Mock(Subscriber)//通過Mock構造一個依賴的物件 def subscriber2 = Mock(Subscriber) def publisher = new Publisher() publisher.add(subscriber1) publisher.add(subscriber2) when: publisher.fire("event") then: 1 * subscriber1.receive("event")//驗證方法名為receive,以及引數"event"為的方法執行一次 1 * subscriber2.receive("event") }
expect程式碼塊:
expect: Math.max(1, 2) == 2
cleanup程式碼塊
given: def file = new File("/some/path") file.createNewFile() // ... cleanup: file.delete()
where程式碼塊,可能是Spock最厲害的地方了:
def "computing the maximum of two numbers"() { expect: Math.max(a, b) == c where: a << [5, 3]//執行兩次測試,值依次為5,3,下面類似 b << [1, 9] c << [5, 9] }
測試的時候幫助方法的處理:
def "offered PC matches preferred configuration"() { when: def pc = shop.buyPc() then: pc.vendor == "Sunny" pc.clockRate >= 2333 pc.ram >= 4096 pc.os == "Linux" }
額外定義一個幫助方法:
def "offered PC matches preferred configuration"() { when: def pc = shop.buyPc() then: matchesPreferredConfiguration(pc) } def matchesPreferredConfiguration(pc) { pc.vendor == "Sunny" && pc.clockRate >= 2333 && pc.ram >= 4096 && pc.os == "Linux" }
輸出結果,結果沒有顯示具體的哪個判斷失敗了:
Condition not satisfied: matchesPreferredConfiguration(pc) || false...
使用assert可以達到這個目的:
void matchesPreferredConfiguration(pc) { assert pc.vendor == "Sunny" assert pc.clockRate >= 2333 assert pc.ram >= 4096 assert pc.os == "Linux" }
最終輸出結果,可以看到具體的錯誤條件:
Condition not satisfied: assert pc.clockRate >= 2333 ||| |1666false ...
在預期值斷言的時候使用with:
def "offered PC matches preferred configuration"() { when: def pc = shop.buyPc() then: with(pc) { vendor == "Sunny" clockRate >= 2333 ram >= 406 os == "Linux" } }
使用mock物件的時候也可以用with:
def service = Mock(Service) // has start(), stop(), and doWork() methods def app = new Application(service) // controls the lifecycle of the service when: app.run() then: with(service) { 1 * start() 1 * doWork() 1 * stop() }
當多個預期值一起斷言的時候使用verifyAll :
def "offered PC matches preferred configuration"() { when: def pc = shop.buyPc() then: verifyAll(pc) { vendor == "Sunny" clockRate >= 2333 ram >= 406 os == "Linux" } }
在沒有物件引數的時候也可以這樣:
expect: verifyAll { 2 == 2 4 == 4 }
文件規範:
given: "open a database connection" // code goes here
and可以增加多個使程式碼根據可讀性:
given: "open a database connection" // code goes here and: "seed the customer table" // code goes here and: "seed the product table" // code goes here
測試行為規範的一般模式:
給定條件....
當怎麼樣的時候...
應該怎樣
given: "an empty bank account" // ... when: "the account is credited $10" // ... then: "the account's balance is $10" // ...
擴充套件註解:
@Timeout 設定方法執行的時間
@Ignore 跳過這個測試方法和JUnit類似
@IgnoreRest 跳過其他所有的測試方法,只執行這個測試方法
資料驅動測試
在where裡面可以通過資料表格,資料管道,指定變數三種情況對不同的測試case進行賦值,特別是在當需要測試很多種情況的的時候,這個模式具有非常高的可讀性和簡潔性:
... where: a | _ 3 | _ 7 | _ 0 | _ b << [5, 0, 0] c = a > b ? a : b
基於測試的互動
場景如下,一個傳送者向多個訂閱者傳送訊息
class PublisherSpec extends Specification { Publisher publisher = new Publisher() Subscriber subscriber = Mock() Subscriber subscriber2 = 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") then: 1 * subscriber.receive("hello") 1 * subscriber2.receive("hello") }
斷言表示式解釋:
1 * subscriber.receive("hello") |||| |||關聯引數 ||關聯目標方法 |關聯目標物件 執行次數
在mock建立的時候可以定義多個互動行為:
class MySpec extends Specification { Subscriber subscriber = Mock { 1 * receive("hello") 1 * receive("goodbye") } }
一個測試程式碼的多個互動通過when區分:
when: publisher.send("message1") then: 1 * subscriber.receive("message1") when: publisher.send("message2") then: 1 * subscriber.receive("message2")
結果輸出:
Too many invocations for: 2 * subscriber.receive(_) (3 invocations)
打樁:
很多時候我們測試的時候不需要執行真正的方法,因為在測試程式碼裡面構建一個真實的物件是比較麻煩的,特別是使用一些依賴注入框架的時候,因此有了打樁的功能:
interface Subscriber { String receive(String message) }
模擬返回值:
subscriber.receive(_) >> "ok"
表示式解釋:
subscriber.receive(_) >> "ok" |||| |||任何引數的這個方法呼叫都會返回"ok" ||argument constraint |method constraint target constraint
需要呼叫多次返回不同結果的時候可以這樣定義:
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
有時候我們測試的一個方法呼叫了類的其他方法,而其他的方法可能依賴了外部的物件,為了避免引入外部物件,可以使用部分mock功能減少構建外部物件的繁瑣:
// this is now the object under specification, not a collaborator MessagePersister persister = Spy { // 這個方法的任意引數呼叫都返回true isPersistable(_) >> true } when: persister.receive("msg") then: // when裡面的receive會呼叫persist,前提是isPersistable是true,因為mock的值是true,這個方法一定會被呼叫 1 * persister.persist("msg")
和springboot整合
spring本身提供了spring-test測試模組,需要和Spock一起使用的話,只需要在Specification類上面加上SpringApplicationConfiguration這個註解就可以了,如果是mvc應用還需加上WebAppConfiguration這個,其他的使用和原來一樣。
maven配置
在pom.xml裡面新增
<!-- Spock依賴 --> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>1.2-groovy-2.4</version> <scope>test</scope> </dependency> <!-- Spock需要的groovy依賴 --> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.15</version> </dependency> <!--...... --> <!--編譯外掛配置: --> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.20.1</version> <configuration> <useFile>false</useFile> <includes> <include>**/*Test.java</include><!--指定JUnit的測試類名稱字尾--> <include>**/*Spec.java</include><!--指定Spock的測試類名稱字尾--> </includes> </configuration> </plugin>
總結
Spock整體上是一個規範描述型的測試框架,測試程式碼非常易讀。對於java程式設計師,上手非常容易,在學習的時候可以先在一個專案上使用,當你發下它的優點的時候,你會盡可能的把其他的測試程式碼也改成用Spock來寫,這個過程可能會花費你更多的時間,但是當你熟悉了以後,它帶給你的收益一定是值得的。不要猶豫,學習新框架最快的方式就是馬上動手。