SpringBoot 單元測試與 Mockito 使用

SpringBoot 單元測試與 Mockito 使用

單元測試應遵循 → AIR 原則html

SpringBoot 測試支持由兩個模塊提供:java

  • spring-boot-test 包含核心項目
  • spring-boot-test-autoconfigure 支持測試的自動配置

一般咱們只要引入 spring-boot-starter-test 依賴就行,它包含了一些經常使用的模塊 Junit、Spring Test、AssertJ、Hamcrest、Mockito 等。web

相關注解

SpringBoot 使用了 Junit4 做爲單元測試框架,因此註解與 Junit4 是一致的。spring

註解 做用
@Test(excepted==xx.class,timeout=毫秒數) 修飾一個方法爲測試方法,excepted參數能夠忽略某些異常類
@Before 在每個測試方法被運行前執行一次
@BeforeClass 在全部測試方法執行前執行
@After 在每個測試方法運行後執行一次
@AfterClass 在全部測試方法執行後執行
@Ignore 修飾的類或方法會被測試運行器忽略
@RunWith 更改測試運行器

@SpringBootTest

SpringBoot提供了一個 @SpringBootTest 註解用於測試 SpringBoot 應用,它能夠用做標準 spring-test @ContextConfiguration 註釋的替代方法,其原理是經過 SpringApplication 在測試中建立ApplicationContext。數據庫

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
}
複製代碼

該註解提供了兩個屬性用於配置:api

  • webEnvironment:指定Web應用環境,它能夠是如下值
    • MOCK:提供一個模擬的 Servlet 環境,內置的 Servlet 容器沒有啓動,配合能夠與@AutoConfigureMockMvc 結合使用,用於基於 MockMvc 的應用程序測試。
    • RANDOM_PORT:加載一個 EmbeddedWebApplicationContext 並提供一個真正嵌入式的 Servlet 環境,隨機端口。
    • DEFINED_PORT:加載一個 EmbeddedWebApplicationContext 並提供一個真正嵌入式的 Servlet 環境,默認端口 8080 或由配置文件指定。
    • NONE:使用 SpringApplication 加載 ApplicationContext,但不提供任何 servlet 環境。
  • classes:指定應用啓動類,一般狀況下無需設置,由於 SpringBoot 會自動搜索,直到找到 @SpringBootApplication 或 @SpringBootConfiguration 註解。

單元測試回滾

若是你添加了 @Transactional 註解,它會在每一個測試方法結束時會進行回滾操做。服務器

可是若是使用 RANDOM_PORT 或 DEFINED_PORT 這種真正的 Servlet 環境,HTTP 客戶端和服務器將在不一樣的線程中運行,從而分離事務。 在這種狀況下,在服務器上啓動的任何事務都不會回滾。app

斷言

JUnit4 結合 Hamcrest 提供了一個全新的斷言語法——assertThat,結合 Hamcrest 提供的匹配符,就能夠表達所有的測試思想。框架

// 通常匹配符
int s = new C().add(1, 1);
// allOf:全部條件必須都成立,測試才經過
assertThat(s, allOf(greaterThan(1), lessThan(3)));
// anyOf:只要有一個條件成立,測試就經過
assertThat(s, anyOf(greaterThan(1), lessThan(1)));
// anything:不管什麼條件,測試都經過
assertThat(s, anything());
// is:變量的值等於指定值時,測試經過
assertThat(s, is(2));
// not:和is相反,變量的值不等於指定值時,測試經過
assertThat(s, not(1));

// 數值匹配符
double d = new C().div(10, 3);
// closeTo:浮點型變量的值在3.0±0.5範圍內,測試經過
assertThat(d, closeTo(3.0, 0.5));
// greaterThan:變量的值大於指定值時,測試經過
assertThat(d, greaterThan(3.0));
// lessThan:變量的值小於指定值時,測試經過
assertThat(d, lessThan(3.5));
// greaterThanOrEuqalTo:變量的值大於等於指定值時,測試經過
assertThat(d, greaterThanOrEqualTo(3.3));
// lessThanOrEqualTo:變量的值小於等於指定值時,測試經過
assertThat(d, lessThanOrEqualTo(3.4));

// 字符串匹配符
String n = new C().getName("Magci");
// containsString:字符串變量中包含指定字符串時,測試經過
assertThat(n, containsString("ci"));
// startsWith:字符串變量以指定字符串開頭時,測試經過
assertThat(n, startsWith("Ma"));
// endsWith:字符串變量以指定字符串結尾時,測試經過
assertThat(n, endsWith("i"));
// euqalTo:字符串變量等於指定字符串時,測試經過
assertThat(n, equalTo("Magci"));
// equalToIgnoringCase:字符串變量在忽略大小寫的狀況下等於指定字符串時,測試經過
assertThat(n, equalToIgnoringCase("magci"));
// equalToIgnoringWhiteSpace:字符串變量在忽略頭尾任意空格的狀況下等於指定字符串時,測試經過
assertThat(n, equalToIgnoringWhiteSpace(" Magci "));

// 集合匹配符
List<String> l = new C().getList("Magci");
// hasItem:Iterable變量中含有指定元素時,測試經過
assertThat(l, hasItem("Magci"));

Map<String, String> m = new C().getMap("mgc", "Magci");
// hasEntry:Map變量中含有指定鍵值對時,測試經過
assertThat(m, hasEntry("mgc", "Magci"));
// hasKey:Map變量中含有指定鍵時,測試經過
assertThat(m, hasKey("mgc"));
// hasValue:Map變量中含有指定值時,測試經過
assertThat(m, hasValue("Magci"));
複製代碼

基本的單元測試例子

下面是一個基本的單元測試例子,對某個方法的返回結果進行斷言:less

@Service
public class UserService {

    public String getName() {
        return "lyTongXue";
    }
    
}
複製代碼
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService service;

    @Test
    public void getName() {
        String name = service.getName();
        assertThat(name,is("lyTongXue"));
    }

}
複製代碼

Controller 測試

Spring 提供了 MockMVC 用於支持 RESTful 風格的 Spring MVC 測試,使用 MockMvcBuilder 來構造MockMvc 實例。MockMvc 有兩個實現:

  • StandaloneMockMvcBuilder:指定 WebApplicationContext,它將會從該上下文獲取相應的控制器並獲得相應的 MockMvc

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest {
        @Autowired
        private WebApplicationContext webApplicationContext;
        private MockMvc mockMvc;
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    } 
    複製代碼
  • DefaultMockMvcBuilder:經過參數指定一組控制器,這樣就不須要從上下文獲取了

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserControllerTest {
        private MockMvc mockMvc;
        @Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
        } 
    }    
    複製代碼

下面是一個簡單的用例,對 UserController 的 /v1/users/{id} 接口進行測試。

@RestController
@RequestMapping("v1/users")
public class UserController {

    @GetMapping("/{id}")
    public User get(@PathVariable("id") String id) {
        return new User(1, "lyTongXue");
    }

    @Data
    @AllArgsConstructor
    public class User {
        private Integer id;
        private String name;
    }

}
複製代碼
// ...
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

    @Autowired
    private WebApplicationContext webApplicationContext;
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getUser() {
        mockMvc.perform(get("/v1/users/1")
                .accept(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isOk())
           .andExpect(content().string(containsString("\"name\":\"lyTongXue\"")));
    }
  
}
複製代碼

方法描述

  • perform:執行一個 RequestBuilder 請求,返回一個 ResultActions 實例對象,可對請求結果進行指望與其它操做

  • get:聲明發送一個 get 請求的方法,更多的請求類型可查閱→MockMvcRequestBuilders 文檔

  • andExpect:添加 ResultMatcher 驗證規則,驗證請求結果是否正確,驗證規則可查閱→MockMvcResultMatchers 文檔

  • andDo:添加 ResultHandler 結果處理器,好比調試時打印結果到控制檯,更多處理器可查閱→MockMvcResultHandlers 文檔

  • andReturn:返回執行請求的結果,該結果是一個恩 MvcResult 實例對象→MvcResult 文檔

Mock 數據

在單元測試中,Service 層的調用每每涉及到對數據庫、中間件等外部依賴。而在單元測試 AIR 原則中,單元測試應該是能夠重複執行的,不該受到外界環境的影響的。此時咱們能夠經過 Mock 一個實現來處理這種狀況。

若是不須要對靜態方法,私有方法等特殊進行驗證測試,則僅僅使用 Spring boot 自帶的 Mockito 便可完成相關的測試數據 Mock。若須要則可使用 PowerMock,簡單實用,結合 Spring 可使用註解注入。

@MockBean

SpringBoot 在執行單元測試時,會將該註解的 Bean 替換掉 IOC 容器中原生 Bean。

例以下面代碼中, ProjectService 中經過 ProjectMapper 的 selectById 方法進行數據庫查詢操做:

@Service
public class ProjectService {

    @Autowired
    private ProjectMapper mapper;

    public ProjectDO detail(String id) {
        return mapper.selectById(id);
    }

}

複製代碼

此時咱們能夠對 Mock 一個 ProjectMapper 對象替換掉 IOC 容器中原生的 Bean,來模擬數據庫查詢操做,如:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProjectServiceTest {
  
    @MockBean
    private ProjectMapper mapper;
    @Autowired
    private ProjectService service;

    @Test
    public void detail() {
        ProjectDemoDO model = new ProjectDemoDO();
        model.setId("1");
        model.setName("dubbo-demo");
        Mockito.when(mapper.selectById("1")).thenReturn(model);
        ProjectDemoDO entity = service.detail("1");
        assertThat(entity.getName(), containsString("dubbo-demo"));
    }

}

複製代碼

Mockito 經常使用方法

Mockito 更多的使用可查看→官方文檔

mock() 對象
List list = mock(List.class);

複製代碼
verify() 驗證互動行爲
@Test
public void mockTest() {
	List list = mock(List.class);
  list.add(1);
  // 驗證 add(1) 互動行爲是否發生
  Mockito.verify(list).add(1);
}

複製代碼
when() 模擬指望結果
@Test
public void mockTest() {
  List list = mock(List.class);
  when(mock.get(0)).thenReturn("hello");
  assertThat(mock.get(0),is("hello"));
}

複製代碼
doThrow() 模擬拋出異常
@Test(expected = RuntimeException.class)
public void mockTest(){
  List list = mock(List.class);
  doThrow(new RuntimeException()).when(list).add(1);
  list.add(1);
}

複製代碼
@Mock 註解

在上面的測試中咱們在每一個測試方法裏都 mock 了一個 List 對象,爲了不重複的 mock,使測試類更具備可讀性,咱們可使用下面的註解方式來快速模擬對象:

// @RunWith(MockitoJUnitRunner.class) 
public class MockitoTest {
    @Mock
    private List list;

    public MockitoTest(){
      	// 初始化 @Mock 註解
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void shorthand(){
        list.add(1);
        verify(list).add(1);
    }
}

複製代碼
when() 參數匹配
@Test
public void mockTest(){
	Comparable comparable = mock(Comparable.class);
  //預設根據不一樣的參數返回不一樣的結果
  when(comparable.compareTo("Test")).thenReturn(1);
  when(comparable.compareTo("Omg")).thenReturn(2);
  assertThat(comparable.compareTo("Test"),is(1));
  assertThat(comparable.compareTo("Omg"),is(2));
  //對於沒有預設的狀況會返回默認值
   assertThat(list.get(1),is(999));
   assertThat(comparable.compareTo("Not stub"),is(0));
}

複製代碼
Answer 修改對未預設的調用返回默認指望
@Test
public void mockTest(){
  //mock對象使用Answer來對未預設的調用返回默認指望值
  List list = mock(List.class,new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
      return 999;
    }
  });
  //下面的get(1)沒有預設,一般狀況下會返回NULL,可是使用了Answer改變了默認指望值
  assertThat(list.get(1),is(999));
  //下面的size()沒有預設,一般狀況下會返回0,可是使用了Answer改變了默認指望值
  assertThat(list.size(),is(999));
}

複製代碼
spy() 監控真實對象

Mock 不是真實的對象,它只是建立了一個虛擬對象,並能夠設置對象行爲。而 Spy是一個真實的對象,但它能夠設置對象行爲。

@Test(expected = IndexOutOfBoundsException.class)
public void mockTest(){
  List list = new LinkedList();
  List spy = spy(list);
  //下面預設的spy.get(0)會報錯,由於會調用真實對象的get(0),因此會拋出越界異常
  when(spy.get(0)).thenReturn(3);
  //使用doReturn-when能夠避免when-thenReturn調用真實對象api
  doReturn(999).when(spy).get(999);
  //預設size()指望值
  when(spy.size()).thenReturn(100);
  //調用真實對象的api
  spy.add(1);
  spy.add(2);
  assertThat(spy.size(),is(100));
  assertThat(spy.size(),is(1));
  assertThat(spy.size(),is(2));
  verify(spy).add(1);
  verify(spy).add(2);
  assertThat(spy.get(999),is(999));
}

複製代碼
reset() 重置 mock
@Test
public void reset_mock(){
  List list = mock(List.class);
  when(list.size()).thenReturn(10);
  list.add(1);
	assertThat(list.size(),is(10));
  //重置mock,清除全部的互動和預設
  reset(list);
  assertThat(list.size(),is(0));
}

複製代碼
times() 驗證調用次數
@Test
public void verifying_number_of_invocations(){
  List list = mock(List.class);
  list.add(1);
  list.add(2);
  list.add(2);
  list.add(3);
  list.add(3);
  list.add(3);
  //驗證是否被調用一次,等效於下面的times(1)
  verify(list).add(1);
  verify(list,times(1)).add(1);
  //驗證是否被調用2次
  verify(list,times(2)).add(2);
  //驗證是否被調用3次
  verify(list,times(3)).add(3);
  //驗證是否從未被調用過
  verify(list,never()).add(4);
  //驗證至少調用一次
  verify(list,atLeastOnce()).add(1);
  //驗證至少調用2次
  verify(list,atLeast(2)).add(2);
  //驗證至多調用3次
  verify(list,atMost(3)).add(3);
}

複製代碼
inOrder() 驗證執行順序
@Test
public void verification_in_order(){
  List list = mock(List.class);
  List list2 = mock(List.class);
  list.add(1);
  list2.add("hello");
  list.add(2);
  list2.add("world");
  //將須要排序的mock對象放入InOrder
  InOrder inOrder = inOrder(list,list2);
  //下面的代碼不能顛倒順序,驗證執行順序
  inOrder.verify(list).add(1);
  inOrder.verify(list2).add("hello");
  inOrder.verify(list).add(2);
  inOrder.verify(list2).add("world");
}

複製代碼
verifyZeroInteractions() 驗證零互動行爲
@Test
 public void mockTest(){
   List list = mock(List.class);
   List list2 = mock(List.class);
   List list3 = mock(List.class);
   list.add(1);
   verify(list).add(1);
   verify(list,never()).add(2);
   //驗證零互動行爲
   verifyZeroInteractions(list2,list3);
 }

複製代碼
verifyNoMoreInteractions() 驗證冗餘互動行爲
@Test(expected = NoInteractionsWanted.class)
public void mockTest(){
  List list = mock(List.class);
  list.add(1);
  list.add(2);
  verify(list,times(2)).add(anyInt());
  //檢查是否有未被驗證的互動行爲,由於add(1)和add(2)都會被上面的anyInt()驗證到,因此下面的代碼會經過
  verifyNoMoreInteractions(list);

  List list2 = mock(List.class);
  list2.add(1);
  list2.add(2);
  verify(list2).add(1);
  //檢查是否有未被驗證的互動行爲,由於add(2)沒有被驗證,因此下面的代碼會失敗拋出異常
  verifyNoMoreInteractions(list2);
}

複製代碼
相關文章
相關標籤/搜索