编写单元测试可以帮助开发人员编写高质量的代码,提升代码质量,减少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. py10函数之嵌套-名称空间作用域

    # 函数是第一类对象:函数名指向的值可以被当中参数传递 # 1.函数名可以被传递# name = 'jason'# x = name# print(x)# print(id(x))# def func ...

  2. 微信小程序-自定义tabbar配置及注意事项

    1.选中要创建tabbar组件的目录,右键选定新建Componen 2.然后编写wxml代码和wxss样式 <cover-view class="tab-bar"> & ...

  3. 使用Kali破解无线密码

    1.查看网卡信息,是否有wlanX网卡ifconfig 2.启动网卡监听模式 airmon-ng start wlan0 检查下是否处于监听模型:ifconfig查看一下,如果网卡名加上了mon后则成 ...

  4. Sup, inf convolution for convex functions

    Let $\Omega$ be a bounded convex domain in $\mathbb{R}^n$. $f:\Omega\rightarrow\mathbb{R}^n$. If $f$ ...

  5. Java 接口与接口的多继承关系

    接口与接口之间是多继承的 注意事项:1. 多个父接口中的抽象方法重复,没关系2. 多个父接口中默认方法重复,子接口必须进行默认方法的覆盖重写 //接口A public interface MyInte ...

  6. echarts——横向柱状堆叠图

    var data = { data: [[320], [120], [220], [150]], legend: ['华为', '中兴', '烽火', '瑞斯'], } var option; var ...

  7. ReactJS单页面应用之项目搭建

    初衷 因接手的项目前端采用reactjs+antd,为把控项目中的各个细节,所以想做一些整理,以免后期遗忘. 创建及启动项目 # 全局安装create-react-app # 如果曾经安装过,可先移除 ...

  8. 在vite中怎么批量注册组件

    1. 在webpack中使用require来获取组件 / 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配 //匹配当前文件夹下的所有.vue文件 注册全局组件 const importF ...

  9. 93、springboot 和springcloud版本对比

    https://start.spring.io/actuator/info 建议转为json

  10. Java方法之递归详解【重点】

    递归详解 A方法调用B方法,我们很容易理解! 递归就是:A方法调用A方法!就是自己调用自己. 利用递归可以用简单的程序来解决一些复杂的问题.它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较 ...