Spock+powermock單元測試筆記(持續更新中)


本篇結合我自身的工作經驗做一個簡單的單測總結

為什么

為什么要做單元測試

單測其實分為兩種,一種是寫業務代碼前寫單測,一種是寫業務代碼后寫單測。

一般來說,應該在寫業務代碼前寫單測,開發前寫單測可以幫助開發者從業務着手縷清編碼思路,不至於跑偏,后人也可以借單測來了解一部分業務邏輯。

而寫業務代碼后寫單測也必不可少,因為要提高單測的行覆蓋率和分支覆蓋率,覆蓋到每一行和每一個分支,以便之后再修改這塊代碼邏輯時可以自行從容。

為什么要使用spock

spock相較於我們之前寫的junit+mockito+powermock/jmockito來說主要有以下幾點優勢

① 代碼量少:spock要結合groovy來使用,groovy可以直接編譯成.class類在JVM中運行。使用groovy開發,代碼更簡潔易懂。當然學習groovy本身就含有額外的學習成本,好在groovy語法與 Java 語言的語法很相似,學習起來非常輕松,學習成本不大。

② 語義性好,可讀性高:基於BDD思想,使用語義標簽嚴格控制流程,是一種強制性的約束

③ 自帶mock:相較於junit,spock自帶mock,當然目前spock做一些高級mock比如靜態類,但也可以引入powermock或jmockito來使用

為什么要使用powermock

主要是為了彌補spock無法做高級mock比如靜態類的缺陷

相較於jmockito,powermock更重量級,更慢,但是powermock功能更強大,編碼更簡單,IDEA支持更好,下面是幾種常見單元測試工具的對比(jmockito編碼可讀性不好,與平時編碼習慣不同)

Mock工具 Mock原理 最小Mock單元 被Mock方法限制 使用難度 IDE支持
Mockito 動態代理 不能Mock私有/靜態和構造方法 較容易 很好
Spock 動態代理 不能Mock私有/靜態和構造方法 較復雜 一般
PowerMock 自定義類加載器 任何方法皆可 較復雜 較好
JMockit 運行時字節碼修改 不能Mock構造方法 較復雜 一般
TestableMock 運行時字節碼修改 方法 任何方法皆可 很容易 一般

表格來自TestableMock官網,以后有機會可以嘗試下TestableMock,不過這些都是工具,選一個自己熟悉趁手的就行

開始

引入依賴

版本號:

<spock.version>1.3-groovy-2.5</spock.version>
<groovy.version>2.5.4</groovy.version>
<powermock.version>2.0.0</powermock.version>

主要依賴:

  <!--test start-->
  <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>4.3.2.RELEASE</version>
      <scope>test</scope>
  </dependency>
  <!-- spock -->
  <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-core</artifactId>
      <version>${spock.version}</version>
      <scope>test</scope>
  </dependency>
  <!-- spock和spring集成 -->
  <dependency>
      <groupId>org.spockframework</groupId>
      <artifactId>spock-spring</artifactId>
      <version>${spock.version}</version>
      <scope>test</scope>
  </dependency>
  <!-- spock的mock需要,版本與spock-core版本需同步更新,可參考mvnrepository.com -->
  <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy</artifactId>
      <version>1.9.3</version>
      <scope>test</scope>
  </dependency>
  <!-- spock依賴的groovy -->
  <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <type>pom</type>
      <version>${groovy.version}</version>
      <scope>test</scope>
      <exclusions>
          <exclusion>
              <artifactId>groovy-test-junit5</artifactId>
              <groupId>org.codehaus.groovy</groupId>
          </exclusion>
          <exclusion>
              <artifactId>groovy-testng</artifactId>
              <groupId>org.codehaus.groovy</groupId>
          </exclusion>
      </exclusions>
  </dependency>
  <!-- PowerMock -->
  <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>2.23.0</version>
      <scope>test</scope>
      <exclusions>
          <exclusion>
              <groupId>net.bytebuddy</groupId>
              <artifactId>byte-buddy</artifactId>
          </exclusion>
          <exclusion>
              <artifactId>byte-buddy-agent</artifactId>
              <groupId>net.bytebuddy</groupId>
          </exclusion>
      </exclusions>
  </dependency>
  <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-core</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
  </dependency>

插件(包含JaCoCo):

  <plugin>
      <groupId>org.codehaus.gmavenplus</groupId>
      <artifactId>gmavenplus-plugin</artifactId>
      <version>1.4</version>
      <executions>
          <execution>
              <goals>
                  <goal>compile</goal>
                  <goal>testCompile</goal>
              </goals>
          </execution>
      </executions>
  </plugin>
  <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.22.2</version>
      <configuration>
          <skip>true</skip>
          <includes>
              <include>**/*Spec.java</include>
              <include>**/*Test.java</include>
          </includes>
      </configuration>
  </plugin>
  <!--JaCoCo-->
  <plugin>
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>0.8.2</version>
      <executions>
          <execution>
              <goals>
                  <goal>prepare-agent</goal>
              </goals>
          </execution>
          <execution>
              <id>report</id>
              <phase>test</phase>
              <goals>
                  <goal>report</goal>
              </goals>
          </execution>
      </executions>
  </plugin>

要注意版本沖突,要自己去排包,上面這部分依賴不一定適合所有人

基礎

1、所有的測試類都需要繼承Specification

class MyFirstTest extends Specification {
 // fields
 // fixture methods
 // feature methods
 // helper methods
}

2、固定的幾個方法

•def setupSpec() {} // 只運行一次 在第一個Feature 執行前

•def setup() {} // 每個Feature 運行前

•def cleanup() {} //每個Feature 運行后

•def cleanupSpec() {} //只運行一次 在最后一個Feature 執行后

3、幾個固定方法的調用順序

•super.setupSpec

•sub.setupSpec

•super.setup

•sub.setup

待執行的Feature方法

•sub.cleanup

•super.cleanup

•sub.cleanupSpec

•super.cleanupSpec

4、Feature方法包含的四個部分

•Setup

•Stimulus

•Response

•Cleanup

5、核心Blocks

•given:輸入條件(前置參數)

•when、then |expect

when: 執行行為(mock接口、真實調用)

then:輸出條件(驗證結果)

•where、with:既能覆蓋多種分支,又可以對復雜對象的屬性進行驗證

where:通過表格的方式測試多種分支

with:驗證復雜返回對象使用

•and:銜接上個標簽,補充的作用

•cleanup : 清理必要的資源,一定會執行的block

image-20211126114638661

6、一個示例

def userDao = Mock(UserDao)

def "當輸入的用戶id為:#uid 時返回的郵編是:#postCodeResult,處理后的電話號碼是:#telephoneResult"() {

 given: "mock掉接口返回的用戶信息"
 userDao.getUserInfo() >> users

 when: "調用獲取用戶信息方法"
 def response = userService.getUserById(uid)

 then: "驗證返回結果是否符合預期值"
 with(response) {
     postCode == postCodeResult
     telephone == telephoneResult
 }

 where: "表格方式驗證用戶信息的分支場景"
 uid | users || postCodeResult | telephoneResult
 1 | getUser("上海", "13866667777") || 200000 | "138****7777"
 1 | getUser("北京", "13811112222") || 100000 | "138****2222"
 2 | getUser("南京", "13833334444") || 0 | null

}

def getUser(String province, String telephone){
	return [new UserDTO(id: 1, name: "張三", province: province, telephone: telephone)]
}

高級

1、mock

spock自帶mock,使用起來也非常簡單

// 前置mock,被mock后的對象不會走原來的方法,會直接跳過,若有返回值會直接返回null
def userDao = Mock(UserDao)

// 在單測中直接使用 >> obj,則表示直接放回obj
userDao.getUserInfo() >> users

2、spy

// 前置spy,被spy后的對象會走原來的方法,但如果有對其中的方法做mock,則會直接放回mock后的結果obj
def userDao = Spy(UserDao)

// 在單測中直接使用 >> obj,則表示直接放回obj,其他方法會正常走
userDao.getUserInfo() >> users

3、stub

存根是使協作者以某種方式響應方法調用的行為。當存根方法時,你不關心該方法是否以及將被調用多少次;你只是希望它在被調用時返回一些值

Stub()存根方法也是一個虛擬類,比Mock()更簡單一些,只返回事先准備好的假數據,不提供交互驗證(即該方法是否被調用以及將被調用多少次),使用存根Stub只能驗證狀態(例如測試方法返回的結果數據是否正確,list大小,是否符合斷言等)。

所以Mock比Stub的功能更多一些,但如果我們只是驗證結果使用Stub就足夠了,用法和Mock一樣,而且更輕量一些。

一般情況下,我們都只需要使用到mock就行,如果遇到要mock被測類的其他方法時,可以考慮使用spy

4、exception測試

Spock內置thrown()方法,可以捕獲調用業務代碼拋出的預期異常並驗證

then: "捕獲異常並設置需要驗證的異常值"
        def exception = thrown(ClientTimeOutException)
        exception.errorCode == expectedErrCode
        exception.errorMessage == expectedMessage

5、viod

void方法的測試不能像前面幾篇介紹的那樣在then標簽里驗證返回結果,因為void方法沒有返回值

一般來說無返回值的方法,內部邏輯會修改入參的屬性值,比如參數是個對象,那代碼里可能會修改它的屬性值,雖然沒有返回,但還是可以通過校驗入參的屬性來測試void方法

還有一種更有效的測試方式,就是驗證方法內部邏輯和流程是否符合預期,比如:

  • 應該走到哪個分支邏輯?
  • 是否執行了這一行代碼?
  • for循環中的代碼執行了幾次?
  • 變量在方法內部的變化情況?
then: "驗證調用獲取最新匯率接口的行為是否符合預期: 一共調用2次, 第一次輸出的匯率是0.1413, 第二次是0.1421"
	2 * moneyDAO.getExchangeByCountry(_) >> 0.1413 >> 0.1421

上面這個就是驗證某方法在待測方法內部執行了2次,2次的結果分別是什么,驗證結果還有下面這種寫法

使用>>>后面接中括號,中括號里面的順序就是執行結果的順序

then: "驗證調用獲取最新匯率接口的行為是否符合預期: 一共調用2次, 第一次輸出的匯率是0.1413, 第二次是0.1421"
	2 * moneyDAO.getExchangeByCountry(_) >>> [0.1413, 0.1421]

6、static方法

該部分結合powemock使用即可,在此不介紹powermock的使用語法,介紹一下spock+powermock結合使用的方式

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([LogUtils.class, IDNumberUtils.class])
@SuppressStaticInitializationFor(["com.javakk.spock.util.LogUtils"])

powermock需要使用@RunWith(PowerMockRunner.class)來進行初始化,可惜的是目前單測@RunWith里面只能加一個啟動類,要結合使用spock的話,可以用到powermock的@PowerMockRunnerDelegate注解,將Sputnik.class加進來

另外兩個注解就是power自己要使用到的了,第一個是要mock靜態類的准備,第二個是抑制一些類的初始化

7、抽象方法

抽象方法/父類super方法的mock我們可以借用powermock的能力來實現,例子如下

given:
//        Child child = PowerMockito.spy(new Child())
        Child child = PowerMockito.mock(Child.class)
//		  mock掉抽象類的parentMethod, 返回動態mock值:mockParentReturn
        PowerMockito.when(child.parentMethod()).thenReturn(parentValue)
        PowerMockito.when(child.doSomthing()).thenCallRealMethod()
        expect:
        child.doSomthing() == result

注解整理

spring提供的:@RunWith、@ContextConfiguration、@MockBean、#SpyBean...

1、@RunWith

一個運行器

@RunWith(JUnit4.class) // 就是指用JUnit4來運行
@RunWith(SpringJUnit4ClassRunner.class) // 讓測試運行於Spring測試環境
@RunWith(Suite.class) // 的話就是一套測試集合

2、@ContextConfiguration

Spring整合JUnit4測試時,使用注解引入多個配置文件

單個文件

@ContextConfiguration(Locations="classpath:applicationContext.xml")
@ContextConfiguration(classes = SimpleConfiguration.class)

多個文件

@ContextConfiguration(locations = { "classpath:spring1.xml", "classpath:spring2.xml" })

3、@MockBean

對於一些應用的外部依賴需要進行一些Mock 處理 -> 會自動注入

危害:https://segmentfault.com/a/1190000014122154

@Mock和@MockBean和Mockito.mock()的區別:https://blog.csdn.net/weixin_34101229/article/details/91395871

4、@SpyBean

類上

spock提供的:@UnRoll、@Shared...

1、@UnRoll

@Unroll注解表示展開where標簽下面的每一行測試,作為單獨的case跑

2、@Shared

在測試方法之間共享的數據

未完待續。。。

注意事項

① spock單測目錄要放在groovy目錄下

② spock里面的mock匹配任意參數,使用下划線 _ 即可

③ 注意jar包沖突,有些單測無法運行mock失敗,不一定是編碼問題,可能就是依賴沖突了,或者依賴的版本不對

④ Spock並不支持Mockito和power mock的@InjectMocks@Mock的組合,運行時會報錯,如果你一定要使用對應的功能可以引入Mockitio為Spock專門開發的第三方工具:spock-subjects-collaborators-extension使用@Subject@Collaborator代替@InjectMocks@Mock

參考鏈接

https://javakk.com/category/spock


免責聲明!

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



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