軟件測試是一個應用軟件質量的保證。java開發者開發接口每每忽視接口單元測試。做爲java開發若是會Mock單元測試,那麼你的bug量將會大大下降。spring提供test測試模塊,因此如今小胖哥帶你來玩下springboot下的Mock單元測試,咱們將對controller,service 的單元測試進行實戰操做。java
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
按照上面引入依賴並且scope爲test。該依賴提供了一下類庫git
以上都是在單元測試中常常接觸的類庫。有時間你最好研究一下。web
一個Spring Boot 應用程序是一個Spring ApplicationContext
,通常測試不會超出這個範圍。
測試框架提供一個@SpringBootTest
註解來提供SpringBoot單元測試環境支持。你使用的JUnit版本若是是JUnit 4
不要忘記在測試類上添加@RunWith(SpringRunner.class)
,JUnit 5
就不須要了。默認狀況下,@SpringBootTest不會啓動服務器。您可使用其 webEnvironment
屬性進一步優化測試的運行方式,webEnvironment
相關講解:spring
MOCK
(默認):加載Web ApplicationContext並提供模擬Web環境。該選擇下不會啓動嵌入式服務器。若是類路徑上沒有Web環境,將建立常規非Web的 ApplicationContext
。你能夠配合@AutoConfigureMockMvc
或@AutoConfigureWebTestClient
模擬的Web應用程序。RANDOM_PORT
:加載 WebServerApplicationContext
並提供真實的Web環境,啓用的是隨機web容器端口。DEFINED_PORT
:加載 WebServerApplicationContext
並提供真實的Web環境 和 RANDOM_PORT
不一樣的是啓用你激活的SpringBoot應用端口,一般都聲明在application.yml
配置文件中。NONE
:經過SpringApplication
加載一個ApplicationContext
。但不提供 任何 Web環境(不管是Mock或其餘)。注意事項:若是你的測試帶有@Transactional
註解時,默認狀況下每一個測試方法執行完就會回滾事務。可是當你的 webEnvironment
設置爲RANDOM_PORT
或者 DEFINED_PORT
,也就是隱式地提供了一個真實的servlet web環境時,是不會回滾的。這一點特別重要,請確保不會在生產發佈測試中寫入髒數據。json
言歸正傳,首先咱們編寫了一個 BookService
做爲Service 層
api
package cn.felord.mockspringboot.service; import cn.felord.mockspringboot.entity.Book; /** * The interface Book service. * * @author Dax * @since 14 :54 2019-07-23 */ public interface BookService { /** * Query by title book. * * @param title the title * @return the book */ Book queryByTitle(String title); }
其實現類以下,爲了簡單明瞭沒有測試持久層,若是持久層須要測試注意增刪改須要Spring事務註解@Transactional
支持以達到測試後回滾的目的。springboot
package cn.felord.mockspringboot.service.impl; import cn.felord.mockspringboot.entity.Book; import cn.felord.mockspringboot.service.BookService; import org.springframework.stereotype.Service; import java.time.LocalDate; /** * @author Dax * @since 14:55 2019-07-23 */ @Service public class BookServiceImpl implements BookService { @Override public Book queryByTitle(String title) { Book book = new Book(); book.setAuthor("dax"); book.setPrice(78.56); book.setReleaseTime(LocalDate.of(2018, 3, 22)); book.setTitle(title); return book; } }
controller層以下:服務器
package cn.felord.mockspringboot.api; import cn.felord.mockspringboot.entity.Book; import cn.felord.mockspringboot.service.BookService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * @author Dax * @since 10:24 2019-07-23 */ @RestController @RequestMapping("/book") public class BookApi { @Resource private BookService bookService; @GetMapping("/get") public Book getBook(String title) { return bookService.queryByTitle(title); } }
咱們在Spring Boot maven項目的單元測試包 test
下對應的類路徑 編寫本身的測試類app
package cn.felord.mockspringboot; import cn.felord.mockspringboot.entity.Book; import cn.felord.mockspringboot.service.BookService; import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.BDDMockito; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import javax.annotation.Resource; import java.time.LocalDate; /** * The type Mock springboot application tests. */ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class MockSpringbootApplicationTests { @Resource private MockMvc mockMvc; @MockBean private BookService bookService; @Test public void bookApiTest() throws Exception { String title = "java learning"; // mockbean 開始模擬 bookServiceMockBean(title); // mockbean 模擬完成 String expect = "{\"title\":\"java learning\",\"author\":\"dax\",\"price\":78.56,\"releaseTime\":\"2018-03-22\"}"; mockMvc.perform(MockMvcRequestBuilders.get("/book/get") .param("title", title)) .andExpect(MockMvcResultMatchers.content() .json(expect)) .andDo(MockMvcResultHandlers.print()); // mockbean 重置 } @Test public void bookServiceTest() { String title = "java learning"; bookServiceMockBean(title); Assertions.assertThat(bookService.queryByTitle("ss").getTitle()).isEqualTo(title); } /** * Mock打樁 * @param title the title */ private void bookServiceMockBean(String title) { Book book = new Book(); book.setAuthor("dax"); book.setPrice(78.56); book.setReleaseTime(LocalDate.of(2018, 3, 22)); book.setTitle(title); BDDMockito.given(bookService.queryByTitle(title)).willReturn(book); } }
測試類前兩個註解不用說,第三個註解@AutoConfigureMockMvc
可能大家很陌生。這個是用來開啓Mock Mvc測試的自動化配置的。框架
而後咱們編寫一個測試方法bookApiTest()
來測試BookApi#getBook(String title)
接口。
邏輯是 MockMvc
執行一個模擬的get請求而後指望結果是expect
Json字符串而且將相應對象打印了出來(下圖1標識)。一旦請求不經過將拋出java.lang.AssertionError
錯誤, 會把指望值(Expected
)跟實際值打印出來(下圖2標識)。若是跟預期相同只會出現下圖1。
有個很常見的情形,在開發中有可能你調用的其餘服務沒有開發完,好比你有個短信發送接口還在辦理短信接口手續,可是你還須要短信接口來進行測試。你能夠經過@MockBean
構建一個抽象接口的實現。拿上面的BookService
來講,假如其實現類邏輯尚未肯定,咱們能夠經過規定其入參以及對應的返回值來模擬這個bean的邏輯,或者根據某個情形下進行某個路由操做的選擇(若是入參是A則結果爲B,若是爲C則D)。這種模擬也被成爲測試打樁。 這裏咱們會用到Mockito
測試場景描述以下:
通常有如下幾種組合:
doThrow(…).when(…)
/ doReturn(…).when(…)
/ doAnswer(…).when(…)
given(…).willReturn(…)
/ given(…).willAnswer(…)
when(…).thenReturn(…)
/ when(…).thenAnswer(…)
其餘都好理解,着重介紹一下Answer
, Answer
正是爲了解決若是入參是A則結果爲B,若是爲C則D這種路由操做的。接下來咱們實操一下 ,跟最開始基本同樣,只是更換成@MockBean
而後利用Mockito
編寫打樁方法void bookServiceMockBean(String title)
,模擬上面BookServiceImpl
實現類。不過模擬的bean每次測試完都會自動重置。並且不能用於模擬在應用程序上下文刷新期間運行的bean的行爲。
而後把這個方法注入controller 測試方法就能夠測試了。
內置的assertj
也是經常使用的斷言,api很是友好,這裏也經過bookServiceTest()
簡單演示了一下
本文中實現了一些簡單的Spring Boot啓用集成測試。 對測試環境的搭建,測試代碼的編寫進行了實戰操做,基本能知足平常開發測試須要,相信你能從本文學到很多東西。
相關的講解代碼能夠從gitee獲取。
也可經過我 我的博客 及時獲取更多的乾貨分享。
關注公衆號:Felordcn獲取更多資訊