第三十五章:SpringBoot与单元测试的小秘密
单元测试
对于开发人员来说是非常熟悉的,我们每天的工作也都是围绕着开发与测试进行的,在最早的时候测试都是采用工具Debug
模式进行调试程序,后来Junit
的诞生也让程序测试发生了很大的变化。我们今天来讲解下基于SpringBoot
结合Junit
怎么来完成单元测试
。
本章目的
基于SpringBoot
平台整合Junit
分别完成客户端
、服务端
的单元测试
。
构建项目
我们首先使用idea工具创建一个SpringBoot
项目,并且添加相关Web、MySQL、JPA依赖,具体pom.xml配置依赖内容如下所示:
.../省略其他配置 <dependencies> <!--web依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--data jpa依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!--druid数据源依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency> <!--lombok依赖--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--MySQL依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--springboot程序测试依赖,创建项目默认添加--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> .../省略其他配置
配置数据库
我们本章的内容需要访问数据库,我们先在src/main/resources
下添加application.yml
配置文件,对应添加数据库配置信息如下所示:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8 username: root password: 123456 #最大活跃数 maxActive: 20 #初始化数量 initialSize: 1 #最大连接等待超时时间 maxWait: 60000 #打开PSCache,并且指定每个连接PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 #通过connectionProperties属性来打开mergeSql功能;慢SQL记录 #connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select 1 from dual testWhileIdle: true testOnBorrow: false testOnReturn: false #配置监控统计拦截的filters,去掉后监控界面sql将无法统计,'wall'用于防火墙 filters: stat, wall, log4j jpa: properties: hibernate: show_sql: true format_sql: true
以上配置都是比较常用到,这里不做多解释了,如果不明白可以去本文底部SpringBoot学习目录
文章内找寻对应的章节。
构建实体
对应数据库内的数据表来创建一个商品基本信息实体,实体内容如下所示:
package com.yuqiyu.chapter35.bean; import lombok.Data; import javax.persistence.*; import java.io.Serializable; /** * 商品基本信息实体 * ======================== * Created with IntelliJ IDEA. * User:恒宇少年 * Date:2017/9/13 * Time:22:20 * 码云:http://git.oschina.net/jnyqy * ======================== */ @Data @Entity @Table(name = "good_infos") public class GoodInfoEntity implements Serializable { //商品编号 @Id @Column(name = "tg_id") @GeneratedValue private Integer tgId; //商品类型编号 @Column(name = "tg_type_id") private Integer typeId; //商品标题 @Column(name = "tg_title") private String title; //商品价格 @Column(name = "tg_price") private double price; //商品排序 @Column(name = "tg_order") private int order; }
构建JPA
基于商品基本信息实体类创建一个JPA接口,该接口继承JpaRepository
接口完成框架通过反向代理模式进行生成实现类,自定义JPA接口内容如下所示:
package com.yuqiyu.chapter35.jpa; import com.yuqiyu.chapter35.bean.GoodInfoEntity; import org.springframework.data.jpa.repository.JpaRepository; /** * 商品jpa * ======================== * Created with IntelliJ IDEA. * User:恒宇少年 * Date:2017/9/13 * Time:22:23 * 码云:http://git.oschina.net/jnyqy * ======================== */ public interface GoodInfoJPA extends JpaRepository<GoodInfoEntity,Integer> { }
构建测试控制器
下面我们开始为单元测试
来做准备工作,先来创建一个SpringMVC
控制器来处理请求,代码如下所示:
package com.yuqiyu.chapter35.controller; import com.yuqiyu.chapter35.bean.GoodInfoEntity; import com.yuqiyu.chapter35.jpa.GoodInfoJPA; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * =============================== * Created with Eclipse. * User:于起宇 * Date:2017/9/13 * Time:18:37 * 简书:http://www.jianshu.com/u/092df3f77bca * ================================ */ @RestController public class TestController { //商品基本信息数据接口 @Autowired private GoodInfoJPA goodInfoJPA; /** * 查询首页内容 * @return */ @RequestMapping(value = "/index") public String index(String name) { return "this is index page" + name; } /** * 查询全部商品 * @return */ @RequestMapping(value = "/all") public List<GoodInfoEntity> selectAll() { return goodInfoJPA.findAll(); } /** * 查询商品详情 * @param goodId * @return */ @RequestMapping(value = "/detail",method = RequestMethod.GET) public GoodInfoEntity selectOne(Integer goodId) { return goodInfoJPA.findOne(goodId); } }
我们在测试控制内注入了GoodInfoJPA
,获得了操作商品基本信息的数据接口代理实例,我们可以通过该代理实例去做一些数据库操作,如上代码selectAll
、detail
方法所示。
在测试控制器内添加了三个测试MVC
方法,我们接下来开始编写单元测试
代码。
编写单元测试
在我们使用idea开发工具构建完成SpringBoot
项目后,会自动为我们添加spring-boot-starter-test
依赖到pom.xml
配置文件内,当然也为我们自动创建了一个测试类,该类内一开始是没有过多的代码的。
下面我们开始基于该测试类进行添加逻辑,代码如下所示:
....//省略依赖导包 /** * 单元测试 */ @RunWith(SpringRunner.class) @SpringBootTest public class Chapter35ApplicationTests { /** * 模拟mvc测试对象 */ private MockMvc mockMvc; /** * web项目上下文 */ @Autowired private WebApplicationContext webApplicationContext; /** * 商品业务数据接口 */ @Autowired private GoodInfoJPA goodInfoJPA; /** * 所有测试方法执行之前执行该方法 */ @Before public void before() { //获取mockmvc对象实例 mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); } }
在上面测试代码中我们从上面开始讲解下,其中@RunWith
这里就不多做解释了,我们最比较常用到的就是这个注解。
@SpringBootTest
这个注解这里要强调下,这是SpringBoot
项目测试的核心注解,标识该测试类以SpringBoot
方式运行,该注解的源码如下所示:
...//省略导包 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @BootstrapWith(SpringBootTestContextBootstrapper.class) public @interface SpringBootTest { @AliasFor("properties") String[] value() default {}; @AliasFor("value") String[] properties() default {}; Class<?>[] classes() default {}; SpringBootTest.WebEnvironment webEnvironment() default SpringBootTest.WebEnvironment.MOCK; public static enum WebEnvironment { MOCK(false), RANDOM_PORT(true), DEFINED_PORT(true), NONE(false); private final boolean embedded; private WebEnvironment(boolean embedded) { this.embedded = embedded; } public boolean isEmbedded() { return this.embedded; } } }
我们可以看到在@SpringBootTest
注解源码中最为重要的就是@BootstrapWith
,该注解才是配置了测试类的启动方式,以及启动时使用实现类的类型。
测试index请求
MockMvc
这个类是一个被final
修饰的类型,该类无法被继承使用。这个类是Spring
为我们提供模拟SpringMVC
请求的实例类,该类则是由MockMvcBuilders
通过WebApplicationContext
实例进行创建的,初始化MockMvc
实例我们可以看下before
方法逻辑。到现在为止我们才是万事俱备就差编写单元测试
逻辑了,我们首先来编写访问/index
请求路径的测试,具体测试代码如下所示:
/** * 测试访问/index地址 * @throws Exception */ @Test public void testIndex() throws Exception { MvcResult mvcResult = mockMvc .perform(// 1 MockMvcRequestBuilders.get("/index") // 2 .param("name","admin") // 3 ) .andReturn();// 4 int status = mvcResult.getResponse().getStatus(); // 5 String responseString = mvcResult.getResponse().getContentAsString(); // 6 Assert.assertEquals("请求错误", 200, status); // 7 Assert.assertEquals("返回结果不一致", "this is index pageadmin", responseString); // 8 }
MockMvc解析
我在上面代码中进行了标记,我们按照标记进行讲解,这样会更明白一些:1
perform
方法其实只是为了构建一个请求,并且返回ResultActions
实例,该实例则是可以获取到请求的返回内容。2
MockMvcRequestBuilders
该抽象类则是可以构建多种请求方式,如:Post
、Get
、Put
、Delete
等常用的请求方式,其中参数则是我们需要请求的本项目的相对路径,/
则是项目请求的根路径。3
param
方法用于在发送请求时携带参数,当然除了该方法还有很多其他的方法,大家可以根据实际请求情况选择调用。4
andReturn
方法则是在发送请求后需要获取放回时调用,该方法返回MvcResult
对象,该对象可以获取到返回的视图名称、返回的Response状态、获取拦截请求的拦截器集合等。5
我们在这里就是使用到了第4
步内的MvcResult
对象实例获取的MockHttpServletResponse
对象从而才得到的Status
状态码。6
同样也是使用MvcResult
实例获取的MockHttpServletResponse
对象从而得到的请求返回的字符串内容。【可以查看rest返回的json数据】7
使用Junit
内部验证类Assert
判断返回的状态码是否正常为2008
判断返回的字符串是否与我们预计的一样。
测试商品详情
直接上代码吧,跟上面的代码几乎一致,如下所示:
/** * 测试查询详情 * @throws Exception */ @Test public void testDetail() throws Exception { MvcResult mvcResult = mockMvc .perform( MockMvcRequestBuilders.get("/detail") .param("goodId","2") ) .andReturn(); // 5 //输出经历的拦截器 HandlerInterceptor[] interceptors = mvcResult.getInterceptors(); System.out.println(interceptors[0].getClass().getName()); int status = mvcResult.getResponse().getStatus(); // 6 String responseString = mvcResult.getResponse().getContentAsString(); // 7 System.out.println("返回内容:"+responseString); Assert.assertEquals("return status not equals 200", 200, status); // 8 }
上面唯一一个部分需要解释下,在上面测试方法内输出了请求经历的拦截器,如果我们配置了多个拦截器这里会根据先后顺序写入到拦截器数组内,其他的MockMvc
测试方法以及参数跟上面测试方法一致。
测试添加
在测试类声明定义全局字段时,我们注入了GoodInfoJPA
实例,当然单元测试也不仅仅是客户端
也就是使用MockMvc
方式进行的,我们也可以直接调用JPA
、Service
进行直接测试。下面我们来测试下商品基本信息的添加,代码如下所示:
/** * 测试添加商品基本信息 */ @Test public void testInsert() { /** * 商品基本信息实体 */ GoodInfoEntity goodInfoEntity = new GoodInfoEntity(); goodInfoEntity.setTitle("西红柿"); goodInfoEntity.setOrder(2); goodInfoEntity.setPrice(5.82); goodInfoEntity.setTypeId(1); goodInfoJPA.save(goodInfoEntity); /** * 测试是否添加成功 * 验证主键是否存在 */ Assert.assertNotNull(goodInfoEntity.getTgId()); }
在上面代码中并没有什么特殊的部分,是我们在使用Data JPA
时用到的save
方法用于执行添加,在添加完成后验证主键的值是否存在,NotNull
时证明添加成功。
测试删除
与添加差别不大,代码如下所示:
/** * 测试删除商品基本信息 */ @Test public void testDelete() { //根据主键删除 goodInfoJPA.delete(3); //验证数据库是否已经删除 Assert.assertNull(goodInfoJPA.findOne(3)); }
在上面代码中,我们根据主键的值进行删除商品的基本信息,执行删除完成后调用selectOne方法查看数据库内是否已经不存在该条数据了。
总结
本章主要介绍了基于SpringBoot
平台的两种单元测试
方式,一种是在服务端
采用Spring注入
方式将需要测试的JPA
或者Service
注入到测试类中,然后调用方法即可。另外一种则是在客户端
采用MockMvc
方式测试Web
请求,根据传递的不用参数以及请求返回对象反馈信息进行验证测试。
本章代码已经上传到码云:
网页地址:http://git.oschina.net/jnyqy/lessons
Git地址:https://git.oschina.net/jnyqy/lessons.git
SpringBoot相关系列文章请访问:目录:SpringBoot学习目录
QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录
SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录
感谢阅读!
欢迎加入QQ技术交流群,共同进步。