编写单元测试可以帮助开发人员编写高质量的代码,提升代码质量,减少Bug,便于重构。Spring Boot提供了一些实用程序和注解,用来帮助我们测试应用程序,在Spring Boot中开启单元测试只需引入spring-boot-starter-test即可,其包含了一些主流的测试库。本文主要介绍基于 Service和Controller的单元测试。

引入spring-boot-starter-test:

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

运行Maven命令dependency:tree可看到其包含了以下依赖:

[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:1.5.9.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:1.5.9.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:1.5.9.RELEASE:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.2.0:test
[INFO] | | +- net.minidev:json-smart:jar:2.2.1:test
[INFO] | | | \- net.minidev:accessors-smart:jar:1.1:test
[INFO] | | |     \- org.ow2.asm:asm:jar:5.0.3:test
[INFO] | | \- org.slf4j:slf4j-api:jar:1.7.25:compile
[INFO] | +- junit:junit:jar:4.12:test
[INFO] | +- org.assertj:assertj-core:jar:2.6.0:test
[INFO] | +- org.mockito:mockito-core:jar:1.10.19:test
[INFO] | | \- org.objenesis:objenesis:jar:2.1:test
[INFO] | +- org.hamcrest:hamcrest-core:jar:1.3:test
[INFO] | +- org.hamcrest:hamcrest-library:jar:1.3:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.4.0:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:4.3.13.RELEASE:compile
[INFO] | \- org.springframework:spring-test:jar:4.3.13.RELEASE:test
  • JUnit,标准的单元测试Java应用程序;

  • Spring Test & Spring Boot Test,对Spring Boot应用程序的单元测试提供支持;

  • Mockito, Java mocking框架,用于模拟任何Spring管理的Bean,比如在单元测试中模拟一个第三方系统Service接口返回的数据,而不会去真正调用第三方系统;

  • AssertJ,一个流畅的assertion库,同时也提供了更多的期望值与测试返回值的比较方式;

  • Hamcrest,库的匹配对象(也称为约束或谓词);

  • JsonPath,提供类似XPath那样的符号来获取JSON数据片段;

  • JSONassert,对JSON对象或者JSON字符串断言的库。

一个标准的Spring Boot测试单元应有如下的代码结构:

import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {

}

知识准备

JUnit4注解

JUnit4中包含了几个比较重要的注解:@BeforeClass、@AfterClass、@Before、@After和@Test。其中, @BeforeClass和@AfterClass在每个类加载的开始和结束时运行,必须为静态方法;而@Before和@After则在每个测试方法开始之前和结束之后运行。见如下例子:

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestApplicationTests {

  @BeforeClass
  public static void beforeClassTest() {
      System.out.println("before class test");
  }
   
  @Before
  public void beforeTest() {
      System.out.println("before test");
  }
   
  @Test
  public void Test1() {
      System.out.println("test 1+1=2");
      Assert.assertEquals(2, 1 + 1);
  }
   
  @Test
  public void Test2() {
      System.out.println("test 2+2=4");
      Assert.assertEquals(4, 2 + 2);
  }
   
  @After
  public void afterTest() {
      System.out.println("after test");
  }
   
  @AfterClass
  public static void afterClassTest() {
      System.out.println("after class test");
  }
}

运行输出如下:

...
before class test
before test
test 1+1=2
after test
before test
test 2+2=4
after test
after class test
...

从上面的输出可以看出各个注解的运行时机。

Assert

上面代码中,我们使用了Assert类提供的assert口方法,下面列出了一些常用的assert方法:

  • assertEquals("message",A,B),判断A对象和B对象是否相等,这个判断在比较两个对象时调用了equals()方法。

  • assertSame("message",A,B),判断A对象与B对象是否相同,使用的是==操作符。

  • assertTrue("message",A),判断A条件是否为真。

  • assertFalse("message",A),判断A条件是否不为真。

  • assertNotNull("message",A),判断A对象是否不为null。

  • assertArrayEquals("message",A,B),判断A数组与B数组是否相等。

MockMvc

下文中,对Controller的测试需要用到MockMvc技术。MockMvc,从字面上来看指的是模拟的MVC,即其可以模拟一个MVC环境,向Controller发送请求然后得到响应。

在单元测试中,使用MockMvc前需要进行初始化,如下所示:

private MockMvc mockMvc;

@Autowired
private WebApplicationContext wac;

@Before
public void setupMockMvc(){
  mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

MockMvc模拟MVC请求

模拟一个get请求:

mockMvc.perform(MockMvcRequestBuilders.get("/hello?name={name}","mrbird"));

模拟一个post请求:

mockMvc.perform(MockMvcRequestBuilders.post("/user/{id}", 1));

模拟文件上传:

mockMvc.perform(MockMvcRequestBuilders.fileUpload("/fileupload").file("file", "文件内容".getBytes("utf-8")));

模拟请求参数:

// 模拟发送一个message参数,值为hello
mockMvc.perform(MockMvcRequestBuilders.get("/hello").param("message", "hello"));
// 模拟提交一个checkbox值,name为hobby,值为sleep和eat
mockMvc.perform(MockMvcRequestBuilders.get("/saveHobby").param("hobby", "sleep", "eat"));

也可以直接使用MultiValueMap构建参数:

MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
params.add("name", "mrbird");
params.add("hobby", "sleep");
params.add("hobby", "eat");
mockMvc.perform(MockMvcRequestBuilders.get("/hobby/save").params(params));

模拟发送JSON参数:

String jsonStr = "{\"username\":\"Dopa\",\"passwd\":\"ac3af72d9f95161a502fd326865c2f15\",\"status\":\"1\"}";
mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(jsonStr.getBytes()));

实际测试中,要手动编写这么长的JSON格式字符串很繁琐也很容易出错,可以借助Spring Boot自带的Jackson技术来序列化一个Java对象(可参考Spring Boot中的JSON技术),如下所示:

User user = new User();
user.setUsername("Dopa");
user.setPasswd("ac3af72d9f95161a502fd326865c2f15");
user.setStatus("1");

String userJson = mapper.writeValueAsString(user);
mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(userJson.getBytes()));

其中,mapper为com.fasterxml.jackson.databind.ObjectMapper对象。

模拟Session和Cookie:

mockMvc.perform(MockMvcRequestBuilders.get("/index").sessionAttr(name, value));
mockMvc.perform(MockMvcRequestBuilders.get("/index").cookie(new Cookie(name, value)));

设置请求的Content-Type:

mockMvc.perform(MockMvcRequestBuilders.get("/index").contentType(MediaType.APPLICATION_JSON_UTF8));

设置返回格式为JSON:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).accept(MediaType.APPLICATION_JSON));

模拟HTTP请求头:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).header(name, values));

MockMvc处理返回结果

期望成功调用,即HTTP Status为200:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
  .andExpect(MockMvcResultMatchers.status().isOk());

期望返回内容是application/json:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
  .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));

检查返回JSON数据中某个值的内容:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
  .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("mrbird"));

这里使用到了jsonPath,$代表了JSON的根节点。更多关于jsonPath的介绍可参考 https://github.com/json-path/JsonPath

判断Controller方法是否返回某视图:

mockMvc.perform(MockMvcRequestBuilders.post("/index"))
  .andExpect(MockMvcResultMatchers.view().name("index.html"));

比较Model:

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1))
  .andExpect(MockMvcResultMatchers.model().size(1))
  .andExpect(MockMvcResultMatchers.model().attributeExists("password"))
  .andExpect(MockMvcResultMatchers.model().attribute("username", "mrbird"));

比较forward或者redirect:

mockMvc.perform(MockMvcRequestBuilders.get("/index"))
  .andExpect(MockMvcResultMatchers.forwardedUrl("index.html"));
// 或者
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
  .andExpect(MockMvcResultMatchers.redirectedUrl("index.html"));

比较返回内容,使用content():

// 返回内容为hello
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
  .andExpect(MockMvcResultMatchers.content().string("hello"));

// 返回内容是XML,并且与xmlCotent一样
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
  .andExpect(MockMvcResultMatchers.content().xml(xmlContent));

// 返回内容是JSON ,并且与jsonContent一样
mockMvc.perform(MockMvcRequestBuilders.get("/index"))
  .andExpect(MockMvcResultMatchers.content().json(jsonContent));

输出响应结果:

mockMvc.perform(MockMvcRequestBuilders.get("/index"))
  .andDo(MockMvcResultHandlers.print());

测试Service

现有如下Service:

@Repository("userService")
public class UserServiceImpl extends BaseService<User> implements UserService {

  @Override
  public User findByName(String userName) {
      Example example = new Example(User.class);
      example.createCriteria().andCondition("username=", userName);
      List<User> userList = this.selectByExample(example);
      if (userList.size() != 0)
          return userList.get(0);
      else
          return null;
  }
}

编写一个该Service的单元测试,测试findByName方法是否有效:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

  @Autowired
  UserService userService;

  @Test
  public void test() {
      User user = this.userService.findByName("scott");
      Assert.assertEquals("用户名为scott", "scott", user.getUsername());
  }
}

运行后,JUnit没有报错说明测试通过,即UserService的findByName方法可行。

此外,和在Controller中引用Service相比,在测试单元中对Service测试完毕后,数据能自动回滚,只需要在测试方法上加上@Transactional注解,比如:

@Test
@Transactional
public void test() {
  User user = new User();
  user.setId(this.userService.getSequence("seq_user"));
  user.setUsername("JUnit");
  user.setPasswd("123456");
  user.setStatus("1");
  user.setCreateTime(new Date());
  this.userService.save(user);
}

运行,测试通过,查看数据库发现数据并没有被插入,这样很好的避免了不必要的数据污染。

测试Controller

现有如下Controller:

@RestController
public class UserController {
  @Autowired
  UserService userService;

  @GetMapping("user/{userName}")
  public User getUserByName(@PathVariable(value = "userName") String userName) {
      return this.userService.findByName(userName);
  }

  @PostMapping("user/save")
  public void saveUser(@RequestBody User user) {
      this.userService.saveUser(user);
  }
}

现在编写一个针对于该ControllergetUserByName(@PathVariable(value = "userName") String userName)方法的测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

  private MockMvc mockMvc;
   
  @Autowired
  private WebApplicationContext wac;
   
  @Before
  public void setupMockMvc(){
      mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
  }
   
  @Test
  public void test() throws Exception {
      mockMvc.perform(
          MockMvcRequestBuilders.get("/user/{userName}", "scott")
          .contentType(MediaType.APPLICATION_JSON_UTF8))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("scott"))
      .andDo(MockMvcResultHandlers.print());
  }
}

运行后,JUnit通过,控制台输出过程如下所示:

MockHttpServletRequest:
    HTTP Method = GET
    Request URI = /user/scott
      Parameters = {}
        Headers = {Content-Type=[application/json;charset=UTF-8]}

Handler:
            Type = demo.springboot.test.controller.UserController
          Method = public demo.springboot.test.domain.User demo.springboot.test.controller.UserController.getUserByName(java.lang.String)

Async:
  Async started = false
    Async result = null

Resolved Exception:
            Type = null

ModelAndView:
      View name = null
            View = null
          Model = null

FlashMap:
      Attributes = null

MockHttpServletResponse:
          Status = 200
  Error message = null
        Headers = {Content-Type=[application/json;charset=UTF-8]}
    Content type = application/json;charset=UTF-8
            Body = {"id":23,"username":"scott","passwd":"ac3af72d9f95161a502fd326865c2f15","createTime":1514535399000,"status":"1"}
  Forwarded URL = null
  Redirected URL = null
        Cookies = []

继续编写一个针对于该ControllersaveUser(@RequestBody User user)方法的测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest {

  private MockMvc mockMvc;
   
  @Autowired
  private WebApplicationContext wac;
   
  @Autowired
  ObjectMapper mapper;
   
   
  @Before
  public void setupMockMvc(){
      mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
  }

  @Test
  @Transactional
  public void test() throws Exception {
      User user = new User();
      user.setUsername("Dopa");
      user.setPasswd("ac3af72d9f95161a502fd326865c2f15");
      user.setStatus("1");
       
      String userJson = mapper.writeValueAsString(user);
      mockMvc.perform(
          MockMvcRequestBuilders.post("/user/save")
          .contentType(MediaType.APPLICATION_JSON_UTF8)
          .content(userJson.getBytes()))
      .andExpect(MockMvcResultMatchers.status().isOk())
      .andDo(MockMvcResultHandlers.print());
  }
}

运行过程如下所示:

MockHttpServletRequest:
    HTTP Method = POST
    Request URI = /user/save
      Parameters = {}
        Headers = {Content-Type=[application/json;charset=UTF-8]}

Handler:
            Type = demo.springboot.test.controller.UserController
          Method = public void demo.springboot.test.controller.UserController.saveUser(demo.springboot.test.domain.User)

Async:
  Async started = false
    Async result = null

Resolved Exception:
            Type = null

ModelAndView:
      View name = null
            View = null
          Model = null

FlashMap:
      Attributes = null

MockHttpServletResponse:
          Status = 200
  Error message = null
        Headers = {}
    Content type = null
            Body =
  Forwarded URL = null
  Redirected URL = null
        Cookies = []

值得注意的是,在一个完整的系统中编写测试单元时,可能需要模拟一个登录用户信息Session,MockMvc也提供了解决方案,可在初始化的时候模拟一个HttpSession:

Spring Boot中编写单元测试的更多相关文章

  1. Spring Boot中使用@Scheduled创建定时任务

    我们在编写Spring Boot应用中经常会遇到这样的场景,比如:我需要定时地发送一些短信.邮件之类的操作,也可能会定时地检查和监控一些标志.参数等. 创建定时任务 在Spring Boot中编写定时 ...

  2. Spring Boot中使用Swagger2构建强大的RESTful API文档

    由于Spring Boot能够快速开发.便捷部署等特性,相信有很大一部分Spring Boot的用户会用来构建RESTful API.而我们构建RESTful API的目的通常都是由于多终端的原因,这 ...

  3. spring boot中使用@Async实现异步调用任务

    本篇文章主要介绍了spring boot中使用@Async实现异步调用任务,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 什么是“异步调用”? “异步调用”对应的是“同步 ...

  4. Spring Boot中使用Spring-data-jpa让数据访问更简单、更优雅

    在上一篇Spring中使用JdbcTemplate访问数据库中介绍了一种基本的数据访问方式,结合构建RESTful API和使用Thymeleaf模板引擎渲染Web视图的内容就已经可以完成App服务端 ...

  5. Spring Boot中使用Spring-data-jpa

    在实际开发过程中,对数据库的操作无非就“增删改查”.就最为普遍的单表操作而言,除了表和字段不同外,语句都是类似的,开发人员需要写大量类似而枯燥的语句来完成业务逻辑. 为了解决这些大量枯燥的数据操作语句 ...

  6. Spring Boot中使用RabbitMQ

    很久没有写Spring Boot的内容了,正好最近在写Spring Cloud Bus的内容,因为内容会有一些相关性,所以先补一篇关于AMQP的整合. Message Broker与AMQP简介 Me ...

  7. 56. spring boot中使用@Async实现异步调用【从零开始学Spring Boot】

    什么是"异步调用"? "异步调用"对应的是"同步调用",同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执 ...

  8. 46. Spring Boot中使用AOP统一处理Web请求日志

    在之前一系列的文章中都是提供了全部的代码,在之后的文章中就提供核心的代码进行讲解.有什么问题大家可以给我留言或者加我QQ,进行咨询. AOP为Aspect Oriented Programming的缩 ...

  9. Spring Boot中的事务是如何实现的

    本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天呢!灯塔君跟大家讲: Spring Boot中的事务是如何实现的 1. 概述 一直在用SpringBoot中的@Transactional来做事 ...

  10. Spring Boot中的事务管理

    原文  http://blog.didispace.com/springboottransactional/ 什么是事务? 我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合 ...

随机推荐

  1. Android蓝牙固件升级 DFU-OTA 固件升级

    1.添加 依赖包: implementation 'no.nordicsemi.android:dfu:1.11.0' 2.DfuService类继承  DfuBaseService package ...

  2. Vue watch监听 date中的变量 与 数组或者对象的数据变化

    直接看下面代码: 1.红色的的为一个对象,watch监听时.需要借助 computed 属性,否则watch监听打印出来的新旧值看不出.(注:方法可以随便写,但是 computed 中 与 watch ...

  3. mysql报错:MySQL server has gone away

    一.报错提示: 二.报错原因: 原因一: 一种可能是发送的 SQL 语句太长,以致超过了 max_allowed_packet 的大小,如果是这种原因,你只要修改 my.cnf,加大 max_allo ...

  4. 深入理解 epoll 原理

    从网卡如何接收数据说起 CPU 如何知道接受了数据? 进程阻塞为什么不占用 CPU 资源? 工作队列 等待队列 唤醒进程 内核接收网络数据全过程 同时监视多个 socket 的方法 select 的监 ...

  5. Navicate破解安装

    1.安装Navicate客户端     2. 注意安装完毕不要打开navicate,打开后后面可能出现rsa public key not found之类的错误,直接点击注册机,选择版本,点击patc ...

  6. laravel关联查询

    1.创建表: -- 创建学生表 CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) C ...

  7. Linux安装证书

    Linux安装 vCenter root CA: 1.访问vCenter管理页面,下载"下载受信任的根 CA 证书" 2.压缩文件内带有数字作为扩展名(.0..1 等)的文件是根证 ...

  8. chklist

    1. 重复检查的必要性.一段代码如果在测试期间没问题,也要间隔几天再去观察是否有问题.2. 如果是集群式的服务,使用定时任务要采用分布式锁,或使用工具随机发送一台都可以.3. 如果任务跑失败,需要支持 ...

  9. 前后端分离--token过期策略方案1

    https://blog.csdn.net/weixin_38827340/article/details/86287496?utm_medium=distribute.pc_aggpage_sear ...

  10. Web前端单词大全

    style 修饰width 宽度height 高度title 想说明的text-align 水平对齐方式center 居中 left 居左 right 居右line-height 垂直对齐方式/行高 ...