Spring-Boot项目中使用Mockito

8次阅读

共计 6620 个字符,预计需要花费 17 分钟才能阅读完成。

本文首发于个人网站:Spring Boot 项目中使用 Mockito

Spring Boot 可以和大部分流行的测试框架协同工作:通过 Spring JUnit 创建单元测试;生成测试数据初始化数据库用于测试;Spring Boot 可以跟 BDD(Behavier Driven Development)工具、Cucumber 和 Spock 协同工作,对应用程序进行测试。

进行软件开发的时候,我们会写很多代码,不过,再过六个月(甚至一年以上)你知道自己的代码怎么运作么?通过测试(单元测试、集成测试、接口测试)可以保证系统的可维护性,当我们修改了某些代码时,通过回归测试可以检查是否引入了新的 bug。总得来说,测试让系统不再是一个黑盒子,让开发人员确认系统可用。

在 web 应用程序中,对 Controller 层的测试一般有两种方法:(1)发送 http 请求;(2)模拟 http 请求对象。第一种方法需要配置回归环境,通过修改代码统计的策略来计算覆盖率;第二种方法是比较正规的思路,但是在我目前经历过的项目中用得不多,今天总结下如何用 Mock 对象测试 Controller 层的代码。

在之前的几篇文章中,我们都使用 bookpub 这个应用程序作为例子,今天也不例外,准备测试它提供的 RESTful 接口是否能返回正确的响应数据。这种测试不同于 单元测试 ,需要为之初始化完整的应用程序上下文、所有的 spring bean 都织入以及数据库中需要有测试数据,一般来说这种测试称之为 集成测试 或者 接口测试

实战

通过 spirng.io 新建的 Spring Boot 项目提供了一个空的测试文件——BookPubApplicationTest.java,内容是:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = BookPubApplication.class)
public class BookPubApplicationTests {
   @Test
   public void contextLoads() {}
}
  • 在 pom 文件中增加 spring-boot-starter-test 依赖,添加 jsonPath 依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>com.jayway.jsonpath</groupId>
   <artifactId>json-path</artifactId>
</dependency>
  • 在 BookPubApplicationTest 中添加测试用例
package com.test.bookpub;

import com.test.bookpub.domain.Book;
import com.test.bookpub.repository.BookRepository;
import org.junit.Before;import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = BookPubApplication.class)
@WebIntegrationTest("server.port:0")
public class BookPubApplicationTests {
    @Autowired
    private WebApplicationContext context;
    @Autowired
    private BookRepository bookRepository;
    @Value("${local.server.port}")
    private int port;

    private MockMvc mockMvc;
    private RestTemplate restTemplate = new TestRestTemplate();

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

   @Test
   public void contextLoads() {assertEquals(1, bookRepository.count());
   }

    @Test
    public void webappBookIsbnApi() {Book book = restTemplate.getForObject("http://localhost:" + port +"/books/9876-5432-1111", Book.class);
        assertNotNull(book);
        assertEquals("中文测试", book.getPublisher().getName());
    }

    @Test
    public void webappPublisherApi() throws Exception {
        //MockHttpServletRequestBuilder.accept 方法是设置客户端可识别的内容类型
        //MockHttpServletRequestBuilder.contentType, 设置请求头中的 Content-Type 字段, 表示请求体的内容类型
        mockMvc.perform(get("/publishers/1")
                .accept(MediaType.APPLICATION_JSON_UTF8))

                .andExpect(status().isOk()) 
               .andExpect(content().string(containsString("中文测试")))
                .andExpect(jsonPath("$.name").value("中文测试"));
    }
}
  • spring boot 项目的代码覆盖率
    使用 cobertura,参考项目的 github 地址:spring boot template
# To create test coverage reports (in target/site/cobertura)
mvn clean cobertura:cobertura test


分析

首先分析在 BookPubApplicationTests 类中用到的注解:

  • @RunWith(SpringJUnit4ClassRunner.class),这是 JUnit 的注解,通过这个注解让 SpringJUnit4ClassRunner 这个类提供 Spring 测试上下文。
  • @SpringApplicationConfiguration(classes = BookPubApplication.class),这是 Spring Boot 注解,为了进行集成测试,需要通过这个注解加载和配置 Spring 应用上下文。这是一个元注解(meta-annoation),它包含了 @ContextConfiguration(loader = SpringApplicationContextLoader.class) 这个注解,测试框架通过这个注解使用 Spring Boot 框架的 SpringApplicationContextLoader 加载器创建应用上下文。
  • @WebIntegrationTest(“server.port:0”),这个注解表示当前的测试是集成测试(integration test),因此需要初始化完整的上下文并启动应用程序。这个注解一般和 @SpringApplicationConfiguration 一起出现。server.port:0指的是让 Spring Boot 在随机端口上启动 Tomcat 服务,随后在测试中程序通过 @Value(“${local.server.port}”) 获得这个端口号,并赋值给 port 变量。当在 Jenkins 或其他持续集成服务器上运行测试程序时,这种随机获取端口的能力可以提供测试程序的并行性。

了解完测试类的注解,再看看测试类的内部。由于这是 Spring Boot 的测试,因此我们可通过 @Autowired 注解织入任何由 Spring 管理的对象,或者是通过 @Value 设置指定的环境变量的值。在现在这个测试类中,我们定义了 WebApplicationContextBookRepository对象。

每个测试用例用 @Test 注解修饰。在第一个测试用例——contextLoads()方法中,我仅仅需要确认 BookRepository 连接已经建立,并且数据库中已经包含了对应的测试数据。

第二个测试用例用来测试我们提供的 RESTful URL——通过 ISBN 查询一本书,即“/books/{isbn}”。在这个测试用例中我们使用 TestRestTemplate 对象发起 RESTful 请求。

第三个测试用例中展示了如何通过 MockMvc 对象实现跟第二个测试类似的功能。Spring 测试框架提供 MockMvc 对象,可以在不需要客户端 - 服务端请求的情况下进行 MVC 测试,完全在服务端这边就可以执行 Controller 的请求,跟启动了测试服务器一样。

测试开始之前需要建立测试环境,setup 方法被 @Before 修饰。通过 MockMvcBuilders 工具,使用 WebApplicationContext 对象作为参数,创建一个 MockMvc 对象。

MockMvc 对象提供一组工具函数用来执行 assert 判断,都是针对 web 请求的判断。这组工具的使用方式是函数的链式调用,允许程序员将多个测试用例链接在一起,并进行多个判断。在这个例子中我们用到下面的一些工具函数:

  • perform(get(…))建立 web 请求。在我们的第三个用例中,通过 MockMvcRequestBuilder 执行 GET 请求。
  • andExpect(…)可以在 perform(…)函数调用后多次调用,表示对多个条件的判断,这个函数的参数类型是 ResultMatcher 接口,在 MockMvcResultMatchers 这这个类中提供了很多返回 ResultMatcher 接口的工具函数。这个函数使得可以检测同一个 web 请求的多个方面,包括 HTTP 响应状态码(response status),响应的内容类型(content type),会话中存放的值,检验重定向、model 或者 header 的内容等等。这里需要通过第三方库 json-path 检测 JSON 格式的响应数据:检查 json 数据包含正确的元素类型和对应的值,例如 jsonPath(“$.name”).value(“ 中文测试 ”) 用于检查在根目录下有一个名为 name 的节点,并且该节点对应的值是“中文测试”。

一个字符乱码问题

  • 问题描述:通过 spring-boot-starter-data-rest 建立的 repository,取出的汉字是乱码。
  • 分析:使用 postman 和 httpie 验证都没问题,说明是 Mockmvc 的测试用例写得不对,应该主动设置客户端如何解析 HTTP 响应,用 get.accept 方法设置客户端可识别的内容类型,修改后的测试用例如下:
@Test
public void webappPublisherApi() throws Exception {
    //MockHttpServletRequestBuilder.accept 方法是设置客户端可识别的内容类型
    //MockHttpServletRequestBuilder.contentType, 设置请求头中的 Content-Type 字段, 表示请求体的内容类型
    mockMvc.perform(get("/publishers/1")
            .accept(MediaType.APPLICATION_JSON_UTF8))

            .andExpect(status().isOk())
            .andExpect(content().string(containsString("中文测试")))
            .andExpect(jsonPath("$.name").value("中文测试"));
}

参考资料

  1. 基于 Spring-WS 的 Restful API 的集成测试
  2. J2EE 要懂的小事—图解 HTTP 协议
  3. Integration Testing a Spring Boot Application
  4. spring boot project template

Spring Boot 1.x 系列

  1. Spring Boot 的自动配置、Command-line-Runner
  2. 了解 Spring Boot 的自动配置
  3. Spring Boot 的 @PropertySource 注解在整合 Redis 中的使用
  4. Spring Boot 项目中如何定制 HTTP 消息转换器
  5. Spring Boot 整合 Mongodb 提供 Restful 接口
  6. Spring 中 bean 的 scope
  7. Spring Boot 项目中使用事件派发器模式
  8. Spring Boot 提供 RESTful 接口时的错误处理实践
  9. Spring Boot 实战之定制自己的 starter
  10. Spring Boot 项目如何同时支持 HTTP 和 HTTPS 协议
  11. 自定义的 Spring Boot starter 如何设置自动配置注解

本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。

正文完
 0