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

作者: 恒宇少年 | 来源:发表于2017-09-14 00:16 被阅读2667次

    单元测试对于开发人员来说是非常熟悉的,我们每天的工作也都是围绕着开发与测试进行的,在最早的时候测试都是采用工具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核心技术
    007 SpringBoot核心技术学习目录 SpringBoot系统的学习目录,敬请关注点赞!!!

    构建项目

    我们首先使用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学习目录
    感谢阅读!

    相关文章

      网友评论

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

      本文链接:https://www.haomeiwen.com/subject/bsqrsxtx.html