1. 程式人生 > >關於SpringBoot框架下的service層單元測試問題(mockito)

關於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中設定的值。