關於SpringBoot框架下的service層單元測試問題(mockito)
mockito的官方文件:
關於Junit測試業務邏輯層中出現的【方法的輸入輸出沒有規範、測試高度依賴spring boot上下文環境、測試用例不完整等】這些問題,我們使用更完整的測試方法來解決。
學習原因:
針對最近遇到的問題:在SpringBoot框架下,如何脫離Spring的環境進行service層的單元測試,同時面臨著【方法的輸入輸出沒有規範、測試高度依賴spring boot上下文環境、測試用例不完整等】這些問題,查了很多資料之後,發現mockito可以很好的解決我當前遇到的問題。因為在這個過程中,查詢的資料由於使用的mockito的版本不一致,還有寫程式碼使用的框架不一致,導致在學習和實踐測試的過程中走了很多彎路。現在總結如下,提供大家使用,少走彎路。
當前我的程式碼框架是:SpringBoot下,採用SpringMVC三層架構模式,使用SpringDataJPA處理簡化DAO層的編寫,語言為kotlin。
一、單元測試的目標和挑戰
單元測試的思路是在不涉及依賴關係的情況下測試程式碼(隔離性),所以測試程式碼與其他類或者系統的關係應該儘量被消除。一個可行的消除方法是替換掉依賴類(測試替換),也就是說我們可以使用替身來替換掉真正的依賴物件。
使用Mockito可以明顯的簡化對外部依賴的測試類的開發。
二、mockito細節講解
2.1 基於Mockito的Mock測試
維基百科對Mock object定義如下:
In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways.
Mock測試用虛擬的物件來代替真實物件來完成測試工作。為什麼要用虛擬物件來代替真實物件?一是因為由於開發分工的問題,導致測試時真實的物件並不存在,二是因為真實物件的行為不可預知,三是可能真實物件難以建立,四是由於真實物件的響應可能很慢。
通常的單元測試僅僅測試方法的結果,Mock測試在此之上能夠測試方法的行為,例如某個方法是否被呼叫或者某方法被呼叫的次數等。
三、mockito的使用步驟
Mockito是一個用於java程式的Mock測試框架,相對於easymock等Mock框架,採用Mockito框架的測試程式碼更加簡潔,可讀性更強。在pom檔案中新增依賴既可使用。
3.1 新增mockito的maven依賴
需要在 Maven 宣告依賴,你可以在 http://search.maven.org 網站中搜索
g:”org.mockito”, a:”mockito-core” 來得到具體的宣告方式。
下面是2.0.2版本的mockito:
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>2.0.2-beta</version>
<scope>test</scope>
</dependency>
3.2 測試依賴環境的定義
目前為止,有兩種方法可以初始化 fields:
(1)Mockito 現在提供一個 JUnit rule。 使用 Mockito 提供的註解比如 @Mock, @Spy,
@InjectMocks 等等。
(2)用 @RunWith(@MockitoJUnitRunner.class) 標註 JUnit 測試類
在 @Before 之前呼叫 MockitoAnnotations.initMocks(Object)
現在你可以選擇使用一個 rule:
@RunWith(@MockitoJUnitRunner.class)
public class TheTest {
@Rule public MockitoRule mockito = MockitoJUnit.rule();
// ...
}
3.3 使用mockito建立和配置mock物件
@mock為一個interface提供一個虛擬的實現。
@InjectMocks將本test類中的mock(或@mock)注入到被標註的物件中去,也就是說被標註的物件中需要使用標註了mock(或@mock)的物件。
mockito遇到使用註解的欄位會呼叫MockitoAnnotations.initMocks(this) 來初始化該 mock 物件。另外也可以通過使用@RunWith(MockitoJUnitRunner.class)來達到相同的效果。
程式碼見3.4.3
3.4 在測試方法中配置mock
使用測試樁stub來定義在service的實現程式碼中使用到的Repository程式碼的設定返回值:
3.4.1 DAO層
interface ProjectRepository : JpaRepository<Project, tag_t> {
fun findByProjectId(projectId: String): Project
}
//JpaRepository是SpringDataJPA提供的一個類,裡面有自定義的方法,我們整合之後直接呼叫即可,這個不重要,可以直接跳過不要看
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}
3.4.2 service層的實現
//service介面
interface AaaService{
fun create(a:Aaa):Aaa
}
//service層的實現
//注入Aaa類的DAO層
@resource private lateinit var aRepository:AaaRepository
class AaaServiceImpl:Aaa{
override fun create(a:Aaa):Aaa{
return aRepository.save(a)
}
}
3.4.3 測試類程式碼:
@RunWith(MockitoJUnitRunner::class)
class AaaServiceImplTest {
//用於定義被Mock的元件
@Mock private lateinit var aaaRepository: AaaRepository
//mock一個要測試的類物件,同時@Mock註解的會被依賴注入到@InjectMocks註解的類物件中
@InjectMocks
private lateinit var aaaService: ProjectServiceImpl
private lateinit var aaa:Aaa
@Before
fun setUp() {
//用於初始化@Mock註解修飾的元件
MockitoAnnotations.initMocks(this)
//定義類物件
aaa = Aaa()
}
@Test
fun create() {
//這是自定義的一個測試樁stub,定義在service層關於dao層語句的返回值定義,使得service程式碼的測試脫離開dao層。
Mockito.`when`(aaaRepository.save(aaa)).thenReturn(aaa)
//執行方法
val result = aaaService.create(aaa)
//這裡可以多驗證幾種結果
assertEquals(aaa, result)
assertEquals(projectInfo.projectId, result.projectId)
//判斷某個方法是否被呼叫(是否發生互動)
Mockito.verify(aaaRepository).save(aaa)
}
}
至此,mockito的測試步驟就完了,更多的小的雜亂的知識點,在上面給出的網址中。下面我們來看看在測試類中對測試更全面的介紹。
四、多項測試簡介
下面結合Mockito工具的使用來談談Mock中涉及一些重要概念。
Stub:
A stub is a controllable replacement for an existing dependency (or collaborator) in the system. By using a stub, you can test your code without dealing with the dependency directly.
我的理解,Stub用於繞開實際依賴或者某些實際方法的執行。可以用Stub去偽造一個方法來繞過資料庫訪問方法的執行。在Mockito中提供了when語句來實現Stub。例如when(a.func()).thenReturn(1)偽造執行a.func(),當呼叫a.func()時返回值定義為1。
Behavior verification:
Mock測試區別於一般單元測試方法的重要特點是Mock測試可以進行行為驗證。在mockito中採用verify(a, times(n)).func()來驗證物件a的func()方法是否被呼叫n次。
Wrap a real object:
Mock物件只能呼叫Stub方法,而不能呼叫其真實方法,否則會丟擲空指標異常。而Mockito提供了spy機制可以用於監控一個真實物件,此時可以呼叫該物件的真實方法。在Mockito中可以採用@Spy標籤或者呼叫Mockito.spy(T object)方法。
Mock中常用註解有:
- @Mock:用於標識mock物件。
- @InjectMocks:將用@Mock標註的mock物件,注入到被某個被該註解標註的測試的物件中。
- @Spy:用來標註某個被@Mockito標註的真實物件。
4.2 mockito的限制
Mockito當然也有一定的限制。而下面三種資料型別則不能夠被測試
final classes
anonymous classes
primitive types
doReturn().when()和when().thenReturn()的區別。後者會呼叫物件的真實api,而前者遇到函式呼叫直接返回doReturn中設定的值。