Spock-高質量單元測試實操篇


 

spock的介紹

<img src="https://tva1.sinaimg.cn/large/008eGmZEgy1gmipihftrbj30xc0gi764.jpg" style="zoom:25%;" />

spock與junit等單元測試框架一樣都是java生態內比較流行的單元測試框架,不同點在於spock基於groovy動態語言,這使得spock相較於傳統Java單元測試框架具備了更強的動態能力,從語法風格來比較 spock 單元測試的語義性更強,代碼本身更能簡潔直觀的體現出測試用例。下對spock與傳統java單元測試進行對比。

傳統的java單元測試

    @Test public void testVersionFilter() { PowerMockito.mockStatic(CurrentScope.class, CommonWebScope.class); PowerMockito.when(CurrentScope.appVer()).thenReturn(AppVer.of("4.0.0")); PowerMockito.when(CurrentScope.clientId()).thenReturn(ClientId.ANDROID); PowerMockito.when(CommonWebScope.appType()).thenReturn(AppType.ANDROID_CN ); TestResource resource; // 在android ios 最小版本之上 resource = TestResource.builder() .androidMinVersion("3.0.0") .androidMaxVersion("6.0.0") .iosMinVersion("4.0.0") .iosMaxVersion("4.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(true); // 在android ios 最小版本之上 android 在最大版本上 resource = TestResource.builder() .androidMinVersion("3.0.0") .androidMaxVersion("4.0.0") .iosMinVersion("4.0.0") .iosMaxVersion("4.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(false); // 在android 最小版本之下 ios之上 resource = TestResource.builder() .androidMinVersion("7.0.0") .iosMinVersion("3.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(false); PowerMockito.when(CurrentScope.appVer()).thenReturn(AppVer.of("5.0.0")); PowerMockito.when(CurrentScope.clientId()).thenReturn(ClientId.IPHONE); PowerMockito.when(CommonWebScope.appType()).thenReturn(AppType.IOS_KWAI); // 在android ios 最小版本之上 resource = TestResource.builder() .androidMinVersion("3.0.0") .iosMinVersion("4.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(true); resource = TestResource.builder() .androidMinVersion("4.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(true); resource = TestResource.builder() .androidMinVersion("3.0.0") .iosMinVersion("4.0.0") .iosMaxVersion("6.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(true); resource = TestResource.builder() .androidMinVersion("3.0.0") .iosMinVersion("4.0.0") .iosMaxVersion("4.5.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(false); // 在android 最小版本之上 ios之下 resource = TestResource.builder() .androidMinVersion("3.0.0") .iosMinVersion("7.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(false); // 在android 最小版本之下 ios之上 resource = TestResource.builder() .androidMinVersion("7.0.0") .iosMinVersion("3.0.0") .build(); Assertions.assertThat(resource.isValid()).isEqualTo(true); } 

錯誤結果的報告

org.opentest4j.AssertionFailedError: Expecting: <true> to be equal to: <false> but was not. Expected :false Actual :true <Click to see difference> 

基於groovy的spock重寫

    def "測試通用版本過濾"() { setup: PowerMockito.mockStatic(CurrentScope.class, CommonWebScope.class); PowerMockito.when(CurrentScope.appVer()).thenReturn(AppVer.of(appVer)); PowerMockito.when(CurrentScope.clientId()).thenReturn(clientId); PowerMockito.when(CommonWebScope.appType()).thenReturn(appType); when: def valid = new TestResource( androidMinVersion: androidMinVersion, androidMaxVersion: androidMaxVersion, iosMinVersion: iosMinVersion, iosMaxVersion: iosMaxVersion ).isValid() then: valid == checkResult where: appVer | clientId | appType | androidMinVersion | androidMaxVersion | iosMinVersion | iosMaxVersion || checkResult "4.0.0" | ClientId.ANDROID | AppType.ANDROID_CN | "3.0.0" | "6.0.0" | "4.0.0" | "4.0.0" || true "4.0.0" | ClientId.ANDROID | AppType.ANDROID_CN | "3.0.0" | "4.0.0" | "4.0.0" | "4.0.0" || false "4.0.0" | ClientId.ANDROID | AppType.ANDROID_CN | "7.0.0" | null | "3.0.0" | null || false "5.0.0" | ClientId.IPHONE | AppType.IOS | "3.0.0" | null | "4.0.0" | null || true "5.0.0" | ClientId.IPHONE | AppType.IOS | "4.0.0" | null | null | null || true "5.0.0" | ClientId.IPHONE | AppType.IOS | "3.0.0" | null | "4.0.0" | "6.0.0" || true "5.0.0" | ClientId.IPHONE | AppType.IOS | "3.0.0" | null | "4.0.0" | "4.5.0" || false "5.0.0" | ClientId.IPHONE | AppType.IOS | "3.0.0" | null | "7.0.0" | null || false "5.0.0" | ClientId.IPHONE | AppType.IOS | "7.0.0" | null | "3.0.0" | null || true } 

錯誤結果的報告

Condition not satisfied: resource.isValid() == checkResult | | | | | true | false | false com.demo.play.model.activity.TestResource@1c58d7be <Click to see difference> at com.demo.play.model.activity.AdminResourceSpockTest.測試通用版本過濾(AdminResourceSpockTest.groovy:44) 

通過以上傳統java單元測試與spock單元測試的比對可以感受到,spock單元測試更為簡短精煉,語義化更強,在異常用例報告上,spock也更清晰明了。

引入

目前spock主流兩個版本 1.x 及 2.x ,當前 2.x 無法與 PowerMock 協同使用,若有大量靜態方法需要 Mock 請使用 spock 1.x 版本

spock 2.x 版本

由於2.x基於junit5,不再兼容powerMock,目前無太好的靜態方法mock。但是groovy升級到3.0,在閉包等方式上更簡練了。

        <!-- spock 2.0 並會自動引入groovy3.0 --> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-spring</artifactId> <version>2.0-M2-groovy-3.0</version> <scope>test</scope> </dependency> 

spock 1.x 版本(推薦)

spock 1.x 版本可以與 powerMock配合使用,可以滿足基本所有單元測試場景,但不支持 groovy3 tab 閉包

        <!-- spock --> <dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.15</version> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-spring</artifactId> <version>1.2-groovy-2.4</version> <scope>test</scope> </dependency> 

這樣我們可以開始編寫spock單元測試了,在 /src/test/(java|groovy)/ 對應的測試包下新建Groovy單元測試類,下是一個最簡單的spock測試實例

// 繼承 spock.lang.Specification class FileRestTest extends Specification { // 這是一個簡單的單元測試方法 // 這里方法定義方式稱為契約,可以直接用中文 def "單元測試方法"() { expect: Math.max(1, 2) == 3 } } 

Blocks

k2fnQ.png
    def "測試 fileKey 與 host 的映射"() { given: def hostMap = ["play-dev.local.cn": "play-admin-dev"] def fileRest = new FileRest() when: def resultKey = fileRest.parseFileKeyByHostMap(uri, host, hostMap) then: resultKey == checkKey where: uri |host |checkKey "/look-admin/index.html" |"look.test.demo.com" |null "/banner/index.html" |"play-dev.local.cn" |"play-admin-dev/banner/index.html" "/banner/" |"play-dev.local.cn" |"play-admin-dev/banner/index.html" "/banner" |"play-dev.local.cn" |"play-admin-dev/banner/index.html" } 

上源碼展示了一個完整的spock單元測試方法,與java最大的不同spock的單元測試可以擁有given、when、then等塊對單元測試邏輯進行分隔。

分塊 替換 功能 說明
given setup 初始化函數、MOCK 非必要
when expect 執行待測試的函數 when 和 then 必須成對出現
then expect 驗證函數結果 when 和 then 可以被 expect 替換
where   多套測試數據的檢測 spock的特性功能
and   對其余塊進行分隔說明 非必要

用expect替換 when & then

def "測試 fileKey 與 host 的映射"() { given: def hostMap = ["play-dev.local.cn": "play-admin-dev"] def fileRest = new FileRest() expect: def resultKey = fileRest.parseFileKeyByHostMap(uri, host, hostMap) // 在spock expect、then塊中不需要顯示的斷言語句 // 當一個表達式返回為boolean時其自動作為斷言存在 resultKey == checkKey where: ... } 

where

where主要作用用於提供多組測試數據,其可以以數據表或者列表的形式來為參數進行多次賦值。

// 數據表: def "測試最大數"() { expect: Math.max(a,b) == result // 這樣一個單元測試會驗證兩組數據 // 第一組 a = 1 , b = 2, result = 2 // 第二組 a = 3 , b = 0, result = 3 where: a |b |result 1 |2 |2 3 |0 |3 } // 數據列表 def "測試最大數"() { expect: Math.max(a,b) == result // 這樣一個單元測試會驗證兩組數據 // 第一組 a = 1 , b = 2, result = 2 // 第二組 a = 3 , b = 0, result = 3 where: a << [1,3] b << [2,0] result << [2,3] } 

MOCK

spock的mock功能非常強大,語義性較傳統java mock框架更突出。

mock 對象的構造

    // 通過傳入class參數來構建 def person = Mock(Person) def person = Spy(Person) def person = Stub(Person) // 通過聲明類型來構建 Spy\Stub同 Person person = Mock() // 交互式的創建 Spy\Stub同 Person person = Mock { getName() >> "bob" } def person = Mock(Person) { getName() >> "bob" } 

簡單的返回值mock

    def "演示mock"() { given: def resource = Mock(Resource) // >> 用於Mock返回 resource.getResourceId() >> 1L expect: resource.getResourceId() == 1L } 

鏈式mock

    def "演示mock"() { given: def resource = Mock(Resource) // 第一次、二次、三次、三次 分別返回 1、2、3、4 resource.getResourceId() >>> [1L,2L,3L] >> 4L expect: resource.getResourceId() == 1L resource.getResourceId() == 2L resource.getResourceId() == 3L resource.getResourceId() == 4L } def "演示mock"() { given: def resource = Mock(Resource) // 第一次調用 1 * resource.getResourceId() >> 1L // 第二次到第三次 2 * resource.getResourceId() >> 2L // 第四次到第六次 3 * resource.getResourceId() >> 3L expect: resource.getResourceId() == 1L resource.getResourceId() == 2L resource.getResourceId() == 2L resource.getResourceId() == 3L resource.getResourceId() == 3L resource.getResourceId() == 3L } 

異常mock

    def "演示mock"() { given: def resource = Mock(Resource) resource.getResourceId() >> {throw new IllegalArgumentException()} when: resource.getResourceId() then: thrown(IllegalArgumentException) } 

復雜邏輯mock

    def "演示mock"() { given: def person = Mock(Person) person.say(_) >> { args -> "hello " + args[0] } when: def msg = person.say("world") then: msg == "hello world" } 

mock & spy & stub

mock 返回的是一個所有方法都為空值的對象,當我們對一個對象部分方法進行mock,但其余方法希望基於真實對象邏輯時可以采用Spy()。

spy基於一個真實的對象,只有被Mock的方法會按指定值返回,其余方法行為與真實對象一致。

stub與mock的區別在於它只有mock的方法返回對應值,未mock的方法會返回無意義的Stub對象本身,另外mock、spy對象能統計調用次數。

    static class Person { def getName() { "tom" } def getAge() { 18 } } def "演示mock"() { given: def mockPerson = Mock(Person) def spyPerson = Spy(Person) def stubPerson = Stub(Person) mockPerson.getName() >> "bob" spyPerson.getName() >> "bob" stubPerson.getName() >> "bob" expect: mockPerson.getName() == "bob" spyPerson.getName() == "bob" stubPerson.getName() == "bob" mockPerson.getAge() == null spyPerson.getAge() == 18 stubPerson.getAge() != 18 && stubPerson.getAge() != null } 

mock static method (2.x 版本無法支持)

spock 有GroovyMock可以在Groovy代碼范圍內對靜態方法進行mock,但無法進行Java 靜態方法的mock。加入powerMock依賴:

        
        <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.8.21</version> <scope>test</scope> </dependency> <dependency> <groupId>org.objenesis</groupId> <artifactId>objenesis</artifactId> <version>2.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-core</artifactId> <version>2.0.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4-rule</artifactId> <version>2.0.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-classloading-xstream</artifactId> <version>2.0.0</version> <scope>test</scope> </dependency> 

使用powerMock mock 靜態方法

@RunWith(PowerMockRunner.class) @PowerMockRunnerDelegate(Sputnik.class) // 此注解指定要mock的靜態方法類 @PrepareForTest(value = [CurrentScope.class, CommonWebScope.class]) class AdminResourceSpockTest extends Specification { def "測試通用版本過濾"() { setup: // 這里對所有需要mock的靜態方法類進行mock PowerMockito.mockStatic(CurrentScope.class, CommonWebScope.class); // 當調用 CurrentScope.appVer() 時將按thenReturn內的值返回 PowerMockito.when(CurrentScope.appVer()).thenReturn內的值返回(AppVer.of(appVer)); PowerMockito.when(CurrentScope.clientId()).thenReturn(clientId); PowerMockito.when(CommonWebScope.appType()).thenReturn(appType); when: def valid = new TestResource( androidMinVersion: androidMinVersion, androidMaxVersion: androidMaxVersion, iosMinVersion: iosMinVersion, iosMaxVersion: iosMaxVersion ).isValid() then: valid == checkResult; where: ... } } 

assert

spock斷言非常簡練,在then或者expect區塊中,一個簡單的boolean語句就可以作為一個斷言,無需其他工具或關鍵詞

    then: // 簡單的boolean表達式 code == 1 // 調用次數統計 http://spockframework.org/spock/docs/1.3/all_in_one.html#_cardinality // mockObj這個mock對象在調用中被調用過一次,參數中 _ 代表可以是任意值匹配 1 * mockObj.run(_) // 參數中指定傳遞1時被調用過一次 1 * mockObj.run(1) // 被調用過1次及以上,且最多三次 (1..3) * mockObj.run(_) // 至少調用一次 (1.._) * mockObj.run(_) // 任何mock對象中方法名為run的方法被調用一次 1 * _.run(_) 

groovy的一些語言特性

由於groovy具備很多語法特性,可以使得我們單元測試的編寫更簡潔清晰

對象構建

    // 用閉包 def resource = new Resource().with { setResourceId(1L) return it } // 用構造傳參 def resource = new Resource(resourceId: 1L) 

字符串

groovy字符串可以通過 ''' 來編寫無需轉意符的字符,這在我們寫一些測試json數據時幫助很大。

    // java String json = "{\"name\":\"tom\",\"age\":18}" // groovy def json = '''{"name": "tom","age": 18}''' 

列表

    def resourceList = [ new Resource(resourceId: 2L), new Resource(resourceId: 3L), new Resource(resourceId: 1L), new Resource(resourceId: 4L) ] // << 可以替代 list.add resourceList << new Resource(resourceId: 5L) << new Resource(resourceId: 6L) 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM