如何对Springboot项目进行单元测试

1 为什么需要单元测试

在平时的开发当中,一个项目往往包含了大量的方法,可能有成千上万个。如何去保证这些方法产生的结果是我们想要的呢?

传统解决方案:Postman 发报文,System.out打印debug日志,或者眼睛看返回报文

1、眼睛看结果是否正确,瞅瞎不说,也太不智能.我们是高智商程序员,能让代码解决的事情,绝不能靠人工去解决.

2、postman 只能对controller进行测试。controller要正确,前提是service,dao都正确。发现问题太晚,解决成本高。

3、对于一些交易系统,由于交易主键的存在,每次都要更改参数后,再进行测试,效率太低。

4、无法对内部的函数功能做测试

5、postman的测试案例与项目工程不再一起,这些案例只能自己一个人用,无法团队共享

这时,就轮到单元测试闪亮出场了。

  • 测试代码和工程代码在同一工程文件中,便于维护和传承。

  • 使用断言自动检测结果

  • 测试粒度小,可以小到每个函数

  • 测试模块间相互依赖小。开发完一个模块,就可以测试一个模块。妈妈再也不用担心我犯大错了。

2 如何做单元测试

2.1 单元测试框架基础

业界单元测试一般采用基于JUNIT和MockMvc 框架进行, 其中JUNIT 是通用测试框架,主要进行Dao层和Service层测试。MockMvc主要进行Controller层测试.

2.1.1 Junit基本概念

Junit的理解其实很简单,用起来也很方便。关键在于理解几个注解。

  • @Test:将一个普通方法修饰成一个测试方法,以前我们运行程序,需要main方法,而现在,可以直接运行一个被@Test修饰的方法。
    • @Test(excepted=xx.class): xx.class表示异常类,表示测试的方法抛出此异常时,认为是正常的测试通过的
    • @Test(timeout=毫秒数) :测试方法执行时间是否符合预期
  • @BeforeClass: 会在所有的Class方法执行前被执行,static方法
  • @AfterClass:会在所有的Class方法执行之后进行执行,static方法。通常用于清理工作。
  • @Before:会在每一个@Test测试方法被运行前执行一次
  • @After:会在每一个@Test测试方法运行后被执行一次。通常用于资源关闭。
  • @Ignore:所修饰的测试方法会被测试运行器忽略
  • @RunWith:可以更改测试运行器org.junit.runner.Runner。

总结一下,运行顺序为 BeforeClass---->Before----->Test----->Ater----->AfterClass

2.2 Springboot框架单元测试实战

2.2.1 Maven 引包

pom.xml文件中添加spring-boot-starter-test 依赖

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

该依赖为springboot 的stater,会引入一系列相关包。

Unit Test Jar.png

2.2.2 新建测试源码目录

  • test/java 存放test *.java文件
  • test/resourse 存放yml配置文件

新建好目录后,需要在project structrue中Mark这两个资源目录,否则工程会无法正常识别加载这些文件。

project structrue.png

2.2.3 编写单元测试案例

建议:为了让工程目录友好,可以根据不同的服务,新建不同的pakage。Dao、Service和Controller各新建一个*.java文件。

2.2.3.1 编写测试基类
package com.qinghaihu.springbootmybatis;


import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@Transactional
@Rollback
@RunWith(SpringRunner.class)
public class SpringbootUnitTest {


}

解释

1、@SpringbootTest @RunWith(SpringJUnit4ClassRunner.class)

让测试在Spring容器环境下执行测试

2、@Transactional 开启事务,支持工程代码中配置了的注解式事务
3、@Rollback 设置测试后回滚,默认属性为true,即回滚。保证测试完后,自测数据不污染数据库,从而保证测试案例可以重复执行。

2.2.3.2 编写三层模型测试代码

Springboot项目采用经典三层模型搭建,即dao,service,controller。开发顺序通常是这样,测试顺序也是一般按照这个顺序进行。

从单元测试实践经验来说,dao层单元测试通常不会强制要求,而service和controller则必须进行。

2.2.3.2.1 Service层 测试
package com.qinghaihu.springbootmybatis.user;

import com.qinghaihu.springbootmybatis.SpringbootUnitTest;
import com.qinghaihu.springbootmybatis.dao.SysUserDao;
import com.qinghaihu.springbootmybatis.entity.SysUser;
import com.qinghaihu.springbootmybatis.service.SysUserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Date;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @ClassName UserServiceTest
 * @Description TODO
 * @Author:Zhang Lianzhong
 * @Date 2020/4/11 5:12 下午
 * @Version 1.0
 **/

public class UserServiceTest extends SpringbootUnitTest {

    @Autowired
    SysUserService sysUserService;

    @Autowired
    SysUserDao sysUserDao;


    @Test
    public void testInsert(){
        SysUser sysUser = new SysUser();
        String id = "5";
        sysUser.setId(id);
        sysUser.setUsername("qinghaihudream");
        sysUser.setPassword("88888887");
        sysUser.setCreatetime(new Date());
        sysUser.setUserdesc("奔跑吧程序猿");
        sysUser.setUserimg("/user/images/qinghaihu.jpg");
        sysUser.setUserstate(-1);

        boolean insertResult = sysUserService.insertUserInfo(sysUser);
        assertTrue(insertResult);

        SysUser queryedUser = sysUserDao.selectByPrimaryKey(id);
        assertEquals(queryedUser.getUsername(),sysUser.getUsername());
    }

    @Test
    public  void testSelect(){
        SysUser sysUser = new SysUser();
        String id = "5";
        sysUser.setId(id);
        sysUser.setUsername("qinghaihudream");
        sysUser.setPassword("88888887");
        sysUser.setCreatetime(new Date());
        sysUser.setUserdesc("奔跑吧程序猿");
        sysUser.setUserimg("/user/images/qinghaihu.jpg");
        sysUser.setUserstate(-1);

        int insertResult = sysUserDao.insert(sysUser);
        assertEquals(insertResult,1);

        SysUser queryedUser = sysUserService.selectById(id);
        assertEquals(queryedUser.getUsername(),sysUser.getUsername());

    }

    @Test
    public void testUpdate() {

        //准备测试数据
        String id = "4";
        String orgUserDesc = "你好程序猿";
        String updateUserDesc = "奔跑吧程序猿";

        SysUser orgSysUser = new SysUser();
        orgSysUser.setId(id);
        orgSysUser.setUsername("qinghaihu");
        orgSysUser.setPassword("88889998");
        orgSysUser.setCreatetime(new Date());
        orgSysUser.setUserdesc(orgUserDesc);
        orgSysUser.setUserimg("/user/images/qinghaihu.jpg");
        orgSysUser.setUserstate(-1);
        int insertCount = sysUserDao.insert(orgSysUser);
        assertEquals(insertCount,1);

        SysUser updateSysUser = new SysUser();
        updateSysUser.setId(id);
        updateSysUser.setUsername("qinghaihu");
        updateSysUser.setPassword("88889998");
        updateSysUser.setCreatetime(new Date());
        updateSysUser.setUserdesc(updateUserDesc);
        updateSysUser.setUserimg("/user/images/qinghaihu.jpg");
        updateSysUser.setUserstate(-1);


        boolean updateResult = sysUserService.updateUserInfo(updateSysUser);
        assertTrue(updateResult);

        //检查数据是否更新
        SysUser queryedUser = sysUserService.selectById(id);
        assertEquals(queryedUser.getUserdesc(),updateUserDesc);


    }

    @Test
    public void testdelete() {
        SysUser sysUser = new SysUser();
        String id = "5";
        sysUser.setId(id);
        sysUser.setUsername("qinghaihudream");
        sysUser.setPassword("88888887");
        sysUser.setCreatetime(new Date());
        sysUser.setUserdesc("奔跑吧程序猿");
        sysUser.setUserimg("/user/images/qinghaihu.jpg");
        sysUser.setUserstate(-1);

        boolean insertResult = sysUserService.insertUserInfo(sysUser);
        assertTrue(insertResult);

        boolean deleteResult = sysUserService.deleteById(id);
        assertTrue(deleteResult);

        //检查数据是否删除
        SysUser queryedUser = sysUserService.selectById(id);
        assertNull(queryedUser);

    }


}

2.2.3.2.2 Controller层测试
package com.qinghaihu.springbootmybatis.user;

import com.qinghaihu.springbootmybatis.SpringbootUnitTest;
import com.qinghaihu.springbootmybatis.dao.SysUserDao;
import com.qinghaihu.springbootmybatis.entity.SysUser;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.util.Date;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
/**
 * @ClassName UserControllerTest
 * @Description TODO
 * @Author:Zhang Lianzhong
 * @Date 2020/4/11 6:13 下午
 * @Version 1.0
 **/
public class UserControllerTest extends SpringbootUnitTest {

    @Autowired
    SysUserDao sysUserDao;

    @Autowired
    protected WebApplicationContext wac;

    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.webAppContextSetup(wac).build();   //初始化MockMvc对象
    }



    @Test
    public void testQuery() throws Exception {
        //准备测试数据
        String id = "4";
        String userName = "qinghaihu";
        SysUser orgSysUser = new SysUser();
        orgSysUser.setId(id);
        orgSysUser.setUsername(userName);
        orgSysUser.setPassword("88889998");
        orgSysUser.setCreatetime(new Date());
        orgSysUser.setUserdesc("奔跑吧程序猿");
        orgSysUser.setUserimg("/user/images/qinghaihu.jpg");
        orgSysUser.setUserstate(-1);
        int insertCount = sysUserDao.insert(orgSysUser);
        assertEquals(insertCount,1);


        RequestBuilder request = null;

        /*
         * 1、MockMvcRequestBuilders.post("XXX")构造一个post请求。
         * 2、mockMvc.perform执行一个请求。
         * 3、ResultActions.param添加请求传值
         * 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE)设置返回类型
         * 5、ResultActions.andExpect添加执行完成后的断言。
         * 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
         *    比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
         * 7、ResultActions.andReturn表示执行完成后返回相应的结果。
         */
        //post提交一个people
        request = post("/SysUser/query")
                .param("id",id);
        String json = mvc.perform(request)
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.username",is(userName)))
                .andDo(print())
                .andReturn().getResponse().getContentAsString();

    }

}

通过@Autowired WebApplicationContext wac:注入web环境的ApplicationContext容器
在@Before修饰的方法中通过MockMvcBuilders.webAppContextSetup(wac).build()创建一个MockMvc 进行测试。

3 工程源码

https://github.com/zhanglianzhong/sprinboot-tech.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容