本篇結合我自身的工作經驗做一個簡單的單測總結
為什么
為什么要做單元測試
單測其實分為兩種,一種是寫業務代碼前寫單測,一種是寫業務代碼后寫單測。
一般來說,應該在寫業務代碼前寫單測,開發前寫單測可以幫助開發者從業務着手縷清編碼思路,不至於跑偏,后人也可以借單測來了解一部分業務邏輯。
而寫業務代碼后寫單測也必不可少,因為要提高單測的行覆蓋率和分支覆蓋率,覆蓋到每一行和每一個分支,以便之后再修改這塊代碼邏輯時可以自行從容。
為什么要使用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
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