本文已同步至我的博客 liaosi's blog-SpringBoot(五)SpringBoot的單元測試
在開發工做中,一般寫好代碼後咱們都會先自測一遍再交給測試部門,自測的方法有多種,也有多種測試工具,好比Postman、Jmeter等,這篇文章主要講對於SpringBoot項目如何使用SpringBoot的單元測試,使用的SpringBoot版本是1.5.7。java
建立一個SpringBoot的Maven項目,個人項目結構爲:
git
SpringBoot的單元測試須要額外添加的依賴是:github
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> //其它依賴此處省略...
下面給出項目的代碼部分。
Javabean類:Book.javaweb
package com.lzumetal.springboot.demodatabase.entity; public class Book { private Integer id; //數據庫主鍵id標識 private String name; //書名 private String author; //做者 private Double price; //價格 //get、set方法省略 }
dao類:BookMapper.javaspring
package com.lzumetal.springboot.demodatabase.mapper; import com.lzumetal.springboot.demodatabase.entity.Book; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface BookMapper { int insert(Book record); List<Book> selectAll(); Book getById(@Param(value = "id") Integer id); }
對應的xml映射文件:BookMapper.xml數據庫
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.lzumetal.springboot.demodatabase.mapper.BookMapper"> <resultMap id="BaseResultMap" type="Book"> <result column="id" jdbcType="INTEGER" property="id" /> <result column="name" jdbcType="VARCHAR" property="name" /> <result column="author" jdbcType="VARCHAR" property="author" /> <result column="price" jdbcType="DOUBLE" property="price" /> </resultMap> <insert id="insert" parameterType="Book"> insert into book (id, name, author, price) values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{author,jdbcType=VARCHAR}, #{price,jdbcType=DOUBLE}) </insert> <select id="selectAll" resultMap="BaseResultMap"> select id, name, author, price from book </select> <select id="getById" resultMap="BaseResultMap"> select id, name, author, price from book WHERE id = #{id} </select> </mapper>
Service類:BookServer.javaapache
package com.lzumetal.springboot.demodatabase.service; import com.lzumetal.springboot.demodatabase.entity.Book; import com.lzumetal.springboot.demodatabase.mapper.BookMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * Created by liaosi on 2017/9/26. */ @Service public class BookService { @Autowired private BookMapper bookMapper; public List<Book> getAllBooks() { return bookMapper.selectAll(); } public Book getById(Integer id) { return bookMapper.getById(id); } }
Controller類:BookController.javajson
package com.lzumetal.springboot.demodatabase.controller; import com.google.gson.Gson; import com.lzumetal.springboot.demodatabase.entity.Book; import com.lzumetal.springboot.demodatabase.service.BookService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * Created by liaosi on 2017/9/26. */ @RestController public class BookController { private static Gson gson = new Gson(); @Autowired private BookService bookService; /** * GET請求+@PathVariable * @param id * @return */ @RequestMapping(value = "/getBook/{id}", method = RequestMethod.GET) public String getBookInfo(@PathVariable("id") Integer id) { return gson.toJson(bookService.getById(id)); } /** * GET請求 * @param id * @return */ @RequestMapping(value = "/getBookInfo2", method = RequestMethod.GET) public String getBoodInfo2(Integer id, String name) { Book book = new Book(); book.setId(id); book.setName(name); return gson.toJson(book); } /** * 普通form表單POST請求 * @param id * @return */ @RequestMapping(value = "/postBookInfo", method = RequestMethod.POST) public String postBoodInfo(Integer id) { return gson.toJson(bookService.getById(id)); } /** * POST請求,參數爲json格式 * @param book * @return */ @RequestMapping(value = "/postJson", method = RequestMethod.POST) public Book postJson(@RequestBody Book book) { return book; } }
SpringBoot項目的啓動類:StartupApplication.javabootstrap
package com.lzumetal.springboot.demodatabase; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication // mapper 接口類包掃描 @MapperScan(basePackages = "com.lzumetal.springboot.demodatabase.mapper") public class StartupApplication { public static void main(String[] args) { SpringApplication.run(StartupApplication.class, args); } }
參考官網文檔:Testing improvements in Spring Boot 1.4springboot
MainTest.java
package com.lzumetal.springboot.demodatabase.test; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.lzumetal.springboot.demodatabase.StartupApplication; import com.lzumetal.springboot.demodatabase.controller.BookController; import com.lzumetal.springboot.demodatabase.entity.Book; import com.lzumetal.springboot.demodatabase.service.BookService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; /* https://spring.io/blog/2016/04/15/testing-improvements-in-spring-boot-1-4 MOCK —提供一個Mock的Servlet環境,內置的Servlet容器並無真實的啓動,主要搭配使用@AutoConfigureMockMvc RANDOM_PORT — 提供一個真實的Servlet環境,也就是說會啓動內置容器,而後使用的是隨機端口 DEFINED_PORT — 這個配置也是提供一個真實的Servlet環境,使用的默認的端口,若是沒有配置就是8080 NONE — 這是個神奇的配置,跟Mock同樣也不提供真實的Servlet環境。 */ @RunWith(SpringRunner.class) @SpringBootTest(classes = StartupApplication.class) public class MainTest { private static Gson gson = new GsonBuilder().setPrettyPrinting().create(); @Autowired private BookService bookService; @Autowired private BookController bookController; @Test public void testBookService() { List<Book> allBooks = bookService.getAllBooks(); System.out.println(gson.toJson(allBooks)); } @Test public void testBookController() { String s = bookController.getBookInfo(1); System.out.println(s); } }
官網上的說明:
@RunWith(SpringRunner.class) tells JUnit to run using Spring’s testing support. SpringRunner is the new name for SpringJUnit4ClassRunner, it’s just a bit easier on the eye.
@SpringBootTest is saying 「bootstrap with Spring Boot’s support」 (e.g. load application.properties and give me all the Spring Boot goodness)
The webEnvironment attribute allows specific 「web environments」 to be configured for the test. You can start tests with a MOCK servlet environment or with a real HTTP server running on either a RANDOM_PORT or a DEFINED_PORT.
If we want to load a specific configuration, we can use the classes attribute of @SpringBootTest. In this example, we’ve omitted classes which means that the test will first attempt to load @Configuration from any inner-classes, and if that fails, it will search for your primary @SpringBootApplication class.
@RunWith
是junit提供的註解,表示該類是單元測試的執行類在java代碼裏進行REST請求測試,經常使用的好比Apache的HttpClient,可是spring也提供了一種簡單便捷的模板類RestTemplate來進行操做。
RestTemplate是Spring提供的一個web層測試模板類,經過RestTemplate能夠很方便地進行web層功能測試。它支持REST風格的URL,並且具備AnnotationMethodHandlerAdapter的數據轉換器HttpMessageConverters的裝配功能。RestTemplate已默認幫咱們完成了一下數據轉換器的註冊:
在默認狀況下,咱們能夠直接利用以上轉換器對響應數據進行轉換處理。如StringHttpMessageConverter來處理text/plain
;MappingJackson2HttpMessageConverter來處理application/json
;MappingJackson2XmlHttpMessageConverter來處理application/xml
。
而若是咱們像拓展其餘的轉換器如Jaxb2RootElementHttpMessageConverter或MappingJacksonHttpMessageConverter。咱們可使用setMessageConverters(List<HttpMessageConverter<?>> messageConverters)來註冊咱們所需的轉換器。
RestTemplate restTemplate = new RestTemplate(); //獲取RestTemplate默認配置好的全部轉換器 List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters(); //默認的MappingJackson2HttpMessageConverter在第7個 先把它移除掉 messageConverters.remove(6); //添加上GSON的轉換器 messageConverters.add(6, new GsonHttpMessageConverter());
這個簡單的例子展現瞭如何使用GsonHttpMessageConverter替換掉默認用來處理application/json的MappingJackson2HttpMessageConverter。
RestTemplate默認(即便用無參構造器建立實例)是使用java.net
包中的標準Java類做爲底層實現來建立HTTP請求。可是能夠調用它的帶ClientHttpRequestFactory參數的構造器,使用 Apache 的 HttpComponents 或 Netty 和 OkHttp等其它HTTP請求庫。
配置默認實例:
@Bean public RestTemplate restTemplate(){ return new RestTemplate(); }
配置定製實例,構造方法中能夠傳入ClientHttpRequestFactory參數,ClientHttpRequestFactory接口的實現類中存在timeout屬性等
@Bean RestTemplate restTemplate(){ //生成一個設置了鏈接超時時間、請求超時時間、異常最大重試次數的httpClient RequestConfig config = RequestConfig.custom() .setConnectionRequestTimeout(10000) .setConnectTimeout(10000) .setSocketTimeout(30000) .build(); HttpClient httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(config) .setRetryHandler(new DefaultHttpRequestRetryHandler(5, false)) .build(); //使用httpClient建立一個ClientHttpRequestFactory的實現 ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); //ClientHttpRequestFactory做爲參數構造一個使用做爲底層的RestTemplate RestTemplate restTemplate = new RestTemplate(requestFactory);
TestRestTemplate是SpringBoot提供的一個測試模板類,在SpringBoot你既可使用RestTemplate,同時也可使用TestRestTemplate,TestRestTemplate是RestTemplate的一個包裝類,而沒有繼承它,因此不會存在bean注入的問題。若是想在TestRestTemplate中獲取,能夠調用它的getRestTemplate()
方法。在使用了SpringBootTest
註解的狀況下,TestRestTemplate能夠直接使用@Autowired
注入。
在下面的示例中主要介紹如何使用TestRestTemplate進行post和get請求測試。若是想使用RestTemplate也差很少是同樣的方式。
UrlRequestTest.java
package com.lzumetal.springboot.demodatabase.test; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.lzumetal.springboot.demodatabase.StartupApplication; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import java.util.HashMap; import java.util.Map; @RunWith(SpringRunner.class) @SpringBootTest(classes = StartupApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) public class UrlRequestTest { private static Gson gson = new GsonBuilder().setPrettyPrinting().create(); @Autowired private TestRestTemplate testRestTemplate; /** * GET請求+@PathVariable */ @Test public void getRequest() { ResponseEntity<String> entity = testRestTemplate.getForEntity("/getBook/{id}", String.class, 1); System.err.println(entity.getBody()); } /** * GET請求 * getForEntity 和 getForObject 的區別: * getForObject返回結果Controller中的返回類型。 * getForEntity返回結果裏包含了請求頭信息等,同時entity.getBody()的結果已經被轉換成了json字符串 */ @Test public void getRequest2() { String result = testRestTemplate.getForObject("/getBookInfo2?id={id}&name={2}", String.class, 100, "解憂雜貨店"); System.err.println(result); } /** * GET請求除了使用佔位符的方式按次序注入,也能夠經過一個map經過名字注入 */ @Test public void getRequest3() { Map<String, Object> param = new HashMap<>(); param.put("bookid", 20); param.put("name", "呼嘯山莊"); String result = testRestTemplate.getForObject("/getBookInfo2?id={bookid}&name={name}", String.class, param); System.err.println(result); } /** * POST請求 */ @Test public void postRequest() { MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("id",2); String result = testRestTemplate.postForObject("/postBookInfo", param, String.class); System.err.println(result); } /** * POST請求,並帶請求頭 */ @Test public void postRequest2() { HttpHeaders headers = new HttpHeaders(); headers.add("token", "aaaaaaabbbbbbdcccc"); MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("id",2); HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(param, headers); ResponseEntity<String> resultEntity = testRestTemplate.postForEntity("/postBookInfo", entity, String.class); System.err.println("reuslt:" + resultEntity.getBody()); System.err.println("headers:" + resultEntity.getHeaders()); } /** * POST請求,入參是json格式字符串:{"id":2,"name":"Effective Java","author":"Joshua Bloch","price":39.0} */ @Test public void postRequest3() { String jsonStr = "{\"id\":2,\"name\":\"Effective Java\",\"author\":\"Joshua Bloch\",\"price\":39.0}"; HttpHeaders headers = new HttpHeaders(); //設置contentType headers.setContentType(MediaType.valueOf("application/json;UTF-8")); HttpEntity<String> entity = new HttpEntity<String>(jsonStr,headers); String result = testRestTemplate.postForObject("/postJson", entity, String.class); System.err.println(result); } /** * 上傳文件 * * @throws Exception */ @Test public void upload() throws Exception { Resource resource = new FileSystemResource("d:/123.jpg"); MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("files", resource); String result = testRestTemplate.postForObject("/uploadFile", param, String.class); System.out.println(result); } /** * 下載文件 * * @throws Exception */ @Test public void download() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.set("token", "xxxxxx"); HttpEntity formEntity = new HttpEntity(headers); ResponseEntity<byte[]> response = testRestTemplate.exchange("/download?file={1}", HttpMethod.GET, formEntity, byte[].class, "d:/aaa.png"); if (response.getStatusCode() == HttpStatus.OK) { FileUtils.writeByteArrayToFile(new File("d:/123.jpg"), response.getBody()); } } }
官網上的說明:
Note that TestRestTemplate is now available as bean whenever @SpringBootTest is used. It’s pre-configured to resolve relative paths to http://localhost:${local.server.port}. We could have also used the @LocalServerPort annotation to inject the actual port that the server is running on into a test field.
關於webEnvironment,有MOCK、RANDOM_PORT、DEFINED_PORT、NONE四個選項,其中:
關於SpringBoot的單元測試,可能也還會用到@Before
等其它註解,本文再也不全面而深刻的研究,僅展現簡單示例供使用參考。
本文示例代碼已上傳到GitHub: https://github.com/liaosilzu2...