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

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

本章目的

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

构建项目

咱们首先使用idea工具建立一个SpringBoot项目,而且添加相关Web、MySQL、JPA依赖,具体pom.xml配置依赖内容以下所示:git

.../省略其余配置
<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配置文件,对应添加数据库配置信息以下所示:web

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学习目录文章内找寻对应的章节。spring

构建实体

对应数据库内的数据表来建立一个商品基本信息实体,实体内容以下所示:sql

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控制器来处理请求,代码以下所示:json

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配置文件内,固然也为咱们自动建立了一个测试类,该类内一开始是没有过多的代码的。
下面咱们开始基于该测试类进行添加逻辑,代码以下所示:springboot

....//省略依赖导包
/**
 * 单元测试
 */
@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请求,根据传递的不用参数以及请求返回对象反馈信息进行验证测试。

本章代码已经上传到码云:
网页地址:http://git.oschina.net/jnyqy/lessons
Git地址:https://git.oschina.net/jnyqy/lessons.git
SpringBoot相关系列文章请访问:目录:SpringBoot学习目录
QueryDSL相关系列文章请访问:QueryDSL通用查询框架学习目录
SpringDataJPA相关系列文章请访问:目录:SpringDataJPA学习目录
感谢阅读!
欢迎加入QQ技术交流群,共同进步。
QQ技术交流群

相关文章
相关标签/搜索