在开发工作中,通常写好代码后我们都会先自测一遍再交给测试部门,自测的方法有多种,也有多种测试工具,比如Postman、Jmeter等,这篇文章主要讲对于SpringBoot项目如何使用SpringBoot的单元测试,使用的SpringBoot版本是1.5.7。
一.示例项目介绍 创建一个SpringBoot的Maven项目,我的项目结构为:
SpringBoot的单元测试需要额外添加的依赖是:
1 2 3 4 5 6 7 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > //其它依赖此处省略...
下面给出项目的代码部分。 Javabean类:Book.java
1 2 3 4 5 6 7 8 9 10 11 package com.lzumetal.springboot.demodatabase.entity;public class Book { private Integer id; private String name; private String author; private Double price; }
dao类:BookMapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?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.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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;@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.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 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.*;@RestController public class BookController { private static Gson gson = new Gson (); @Autowired private BookService bookService; @RequestMapping(value = "/getBook/{id}", method = RequestMethod.GET) public String getBookInfo (@PathVariable("id") Integer id) { return gson.toJson(bookService.getById(id)); } @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); } @RequestMapping(value = "/postBookInfo", method = RequestMethod.POST) public String postBoodInfo (Integer id) { return gson.toJson(bookService.getById(id)); } @RequestMapping(value = "/postJson", method = RequestMethod.POST) public Book postJson (@RequestBody Book book) { return book; } }
SpringBoot项目的启动类:StartupApplication.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.lzumetal.springboot.demodatabase;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication @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.4
1.测试Service或者Controller MainTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 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;@RunWith(SpringRunner.class) @SpringBootTest(classes = StartupApplication.class) @ActiveProfiles(value = "dev") 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提供的注解,表示该类是单元测试的执行类
SpringRunner是spring-test提供的测试执行单元类(是Spring单元测试中SpringJUnit4ClassRunner的新名字)
SpringBootTest 是执行测试程序的引导类。
@ActiveProfiles是用来指定激活的配置环境。
2.模拟发送REST请求测试 RestTemplate 在java代码里进行REST请求测试,常用的比如Apache的HttpClient,但是spring也提供了一种简单便捷的模板类RestTemplate来进行操作。
RestTemplate是Spring提供的一个web层测试模板类,通过RestTemplate可以很方便地进行web层功能测试。它支持REST风格的URL,而且具有AnnotationMethodHandlerAdapter的数据转换器HttpMessageConverters的装配功能。RestTemplate已默认帮我们完成了一下数据转换器的注册:
ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
SourceHttpMessageConverter
XmlAwareFormHttpMessageConverter
在默认情况下,我们可以直接利用以上转换器对响应数据进行转换处理。如StringHttpMessageConverter来处理text/plain;MappingJackson2HttpMessageConverter来处理application/json;MappingJackson2XmlHttpMessageConverter来处理application/xml。
而如果我们像拓展其他的转换器如Jaxb2RootElementHttpMessageConverter或MappingJacksonHttpMessageConverter。我们可以使用setMessageConverters(List<HttpMessageConverter<?>> messageConverters)来注册我们所需的转换器。
1 2 3 4 5 6 7 8 RestTemplate restTemplate = new RestTemplate ();List <HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();messageConverters.remove(6 ); messageConverters.add(6 , new GsonHttpMessageConverter ());
这个简单的例子展示了如何使用GsonHttpMessageConverter替换掉默认用来处理application/json的MappingJackson2HttpMessageConverter。
RestTemplate默认(即使用无参构造器创建实例)是使用java.net包中的标准Java类作为底层实现来创建HTTP请求。但是可以调用它的带ClientHttpRequestFactory参数的构造器,使用 Apache 的 HttpComponents 或 Netty 和 OkHttp等其它HTTP请求库。
配置默认实例:
1 2 3 4 @Bean public RestTemplate restTemplate () { return new RestTemplate (); }
配置定制实例,构造方法中可以传入ClientHttpRequestFactory参数,ClientHttpRequestFactory接口的实现类中存在timeout属性等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean RestTemplate restTemplate () { RequestConfig config = RequestConfig.custom() .setConnectionRequestTimeout(10000 ) .setConnectTimeout(10000 ) .setSocketTimeout(30000 ) .build(); HttpClient httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(config) .setRetryHandler(new DefaultHttpRequestRetryHandler (5 , false )) .build(); ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory (httpClient); RestTemplate restTemplate = new RestTemplate (requestFactory);
TestRestTemplate TestRestTemplate是SpringBoot提供的一个测试模板类,在SpringBoot你既可以使用RestTemplate,同时也可以使用TestRestTemplate,TestRestTemplate是RestTemplate的一个包装类,而没有继承它,所以不会存在bean注入的问题。如果想在TestRestTemplate中获取,可以调用它的getRestTemplate()方法。在使用了SpringBootTest注解的情况下,TestRestTemplate可以直接使用@Autowired注入。
在下面的示例中主要介绍如何使用TestRestTemplate进行post和get请求测试。如果想使用RestTemplate也差不多是一样的方式。
UrlRequestTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 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; @Test public void getRequest () { ResponseEntity <String> entity = testRestTemplate.getForEntity("/getBook/{id}" , String.class, 1 ); System.err.println(entity.getBody()); } @Test public void getRequest2 () { String result = testRestTemplate.getForObject("/getBookInfo2?id={id}&name={2}" , String.class, 100 , "解忧杂货店" ); System.err.println(result); } @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); } @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); } @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()); } @Test public void postRequest3 () { String jsonStr = "{\"id\":2,\"name\":\"Effective Java\",\"author\":\"Joshua Bloch\",\"price\":39.0}" ; HttpHeaders headers = new HttpHeaders (); 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); } @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); } @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四个选项,其中:
MOCK —提供一个虚拟的Servlet环境,内置的Servlet容器并没有真实的启动
RANDOM_PORT — 提供一个真实的Servlet环境,也就是说会启动内置容器,然后使用的是随机端口
DEFINED_PORT — 这个配置也是提供一个真实的Servlet环境,使用的配置文件中配置的端口,如果没有配置,默认是8080
关于SpringBoot的单元测试,可能也还会用到@Before等其它注解,本文不再全面而深入的研究,仅展示简单示例供使用参考。
本文示例代码已上传到GitHub: https://github.com/liaosilzu2007/spring-boot.git