第三十五章:SpringBoot与单元测试的小秘密

单元测试对于开发人员来说是非常熟悉的,我们每天的工作也都是围绕着开发与测试进行的,在最早的时候测试都是采用工具Debug模式进行调试程序,后来Junit的诞生也让程序测试发生了很大的变化。我们今天来讲解下基于SpringBoot结合Junit怎么来完成单元测试

本章目的

基于SpringBoot平台整合Junit分别完成客户端服务端单元测试

SpringBoot 企业级核心技术学习专题

专题

专题名称

专题描述

001

Spring Boot 核心技术

讲解SpringBoot一些企业级层面的核心组件

002

Spring Boot 核心技术章节源码

Spring Boot 核心技术简书每一篇文章码云对应源码

003

Spring Cloud 核心技术

对Spring Cloud核心技术全面讲解

004

Spring Cloud 核心技术章节源码

Spring Cloud 核心技术简书每一篇文章对应源码

005

QueryDSL 核心技术

全面讲解QueryDSL核心技术以及基于SpringBoot整合SpringDataJPA

006

SpringDataJPA 核心技术

全面讲解SpringDataJPA核心技术

构建项目

我们首先使用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,获得了操作商品基本信息的数据接口代理实例,我们可以通过该代理实例去做一些数据库操作,如上代码selectAlldetail方法所示。 在测试控制器内添加了三个测试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该抽象类则是可以构建多种请求方式,如:PostGetPutDelete等常用的请求方式,其中参数则是我们需要请求的本项目的相对路径,/则是项目请求的根路径。 3 param方法用于在发送请求时携带参数,当然除了该方法还有很多其他的方法,大家可以根据实际请求情况选择调用。 4 andReturn方法则是在发送请求后需要获取放回时调用,该方法返回MvcResult对象,该对象可以获取到返回的视图名称、返回的Response状态、获取拦截请求的拦截器集合等。 5 我们在这里就是使用到了第4步内的MvcResult对象实例获取的MockHttpServletResponse对象从而才得到的Status状态码。 6 同样也是使用MvcResult实例获取的MockHttpServletResponse对象从而得到的请求返回的字符串内容。【可以查看rest返回的json数据】 7 使用Junit内部验证类Assert判断返回的状态码是否正常为200 8 判断返回的字符串是否与我们预计的一样。

测试商品详情

直接上代码吧,跟上面的代码几乎一致,如下所示:

/**
     * 测试查询详情
     * @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方式进行的,我们也可以直接调用JPAService进行直接测试。下面我们来测试下商品基本信息的添加,代码如下所示:

/**
     * 测试添加商品基本信息
     */
    @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请求,根据传递的不用参数以及请求返回对象反馈信息进行验证测试。

本章代码已经上传到码云: SpringBoot配套源码地址:https://gitee.com/hengboy/spring-boot-chapter SpringCloud配套源码地址:https://gitee.com/hengboy/spring-cloud-chapter SpringBoot相关系列文章请访问:目录:SpringBoot学习目录 QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录 SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录 感谢阅读!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Goroutine + Channel 实践

背景 在最近开发的项目中,后端需要编写许多提供HTTP接口的API,另外技术选型相对宽松,因此选择Golang + Beego框架进行开发。之所以选择Golan...

3274
来自专栏大内老A

我的WCF之旅(3):在WCF中实现双工通信

双工(Duplex)模式的消息交换方式体现在消息交换过程中,参与的双方均可以向对方发送消息。基于双工MEP消息交换可以看成是多个基本模式下(比如请求-回复模式和...

1749
来自专栏哲学驱动设计

BaaS API 设计规范

上个月写了一个团队中的 BaaS API 的设计规范,给大家分享下: 目录 1. 引言... 4 1.1. 概要... 4 1.2. 参考资料... 4 1...

21310
来自专栏大内老A

《WCF技术剖析(卷1)》(修订版)目录

第1章 WCF简介 (WCF Overview) 1.1 SOA的基本概念和设计思想 1.2 WCF是对现有Windows平台下分布式通信技术的整合 1.3 构...

1658
来自专栏Java Web

Java 面试知识点解析(七)——Web篇

在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Jav...

30414
来自专栏架构之路

高并发场景下的httpClient优化使用

1.背景 我们有个业务,会调用其他部门提供的一个基于http的服务,日调用量在千万级别。使用了httpclient来完成业务。之前因为qps上不去,就看了一下业...

9796
来自专栏Java技术

【面试题】2018年最全Java面试通关秘籍第二套!

注:本文是从众多面试者的面试经验中整理而来,其中不少是本人出的一些题目,网络资源众多,如有雷同,纯属巧合!禁止一切形式的碰瓷行为!未经允许禁止一切形式的转载和复...

661
来自专栏互联网高可用架构

简单易用的消息队列框架的设计与实现

1442
来自专栏微信公众号:Java团长

Java面试中问及Hibernate与MyBatis的对比,在这里做一下总结

我是一名java开发人员,hibernate以及mybatis都有过学习,在java面试中也被提及问道过,在项目实践中也应用过,现在对hibernate和myb...

822
来自专栏青枫的专栏

day12_JavaWeb设计模式与案例学习笔记

    C/S:客户端 / 服务器 (胖客户端)比如:LOL、CS、魔兽世界。.exe安装文件。     B/S:浏览器 / 服务器(瘦客户端)比如:页游。网页...

592

扫码关注云+社区