本文已同步至我的博客 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...