在Spring Boot项目中使用Spock框架

简介:

Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目中使用该框架写优雅、高效以及DSL化的测试用例。Spock通过@RunWith注解与JUnit框架协同使用,另外,Spock也可以和Mockito(Spring Boot应用的测试——Mockito)协同使用。

在这个小节中我们会利用Spock、Mockito一起编写一些测试用例(包括对Controller的测试和对Repository的测试),感受下Spock的使用。

How Do

  • 根据Building an Application with Spring Boot这篇文章的描述,spring-boot-maven-plugin这个插件同时也支持在Spring Boot框架中使用Groovy语言。
  • 在pom文件中添加Spock框架的依赖
<!-- test -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.spockframework</groupId>
   <artifactId>spock-core</artifactId>
   <scope>test</scope></dependency>
<dependency>
   <groupId>org.spockframework</groupId>
   <artifactId>spock-spring</artifactId>
   <scope>test</scope>
</dependency>
  • 在src/test目录下创建groovy文件夹,在groovy文件夹下创建com/test/bookpub包。
  • 在resources目录下添加packt-books.sql文件,内容如下所示:
INSERT INTO author (id, first_name, last_name) VALUES (5, 'Shrikrishna', 'Holla');
INSERT INTO book (isbn, title, author, publisher) VALUES ('978-1-78398-478-7', 'Orchestrating Docker', 5, 1);
INSERT INTO author (id, first_name, last_name) VALUES (6, 'du', 'qi');
INSERT INTO book (isbn, title, author, publisher) VALUES ('978-1-78528-415-1', 'Spring Boot Recipes', 6, 1);
  • com/test/bookpub目录下创建SpockBookRepositorySpecification.groovy文件,内容是:
package com.test.bookpubimport com.test.bookpub.domain.Author

import com.test.bookpub.domain.Book
import com.test.bookpub.domain.Publisher
import com.test.bookpub.repository.BookRepository
import com.test.bookpub.repository.PublisherRepository
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.context.ConfigurableApplicationContext
import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import spock.lang.Sharedimport spock.lang.Specification
import javax.sql.DataSourceimport javax.transaction.Transactional

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.status;

@WebAppConfiguration
@ContextConfiguration(classes = [BookPubApplication.class,
 TestMockBeansConfig.class],loader = SpringApplicationContextLoader.class)
class SpockBookRepositorySpecification extends Specification {
    @Autowired
    private ConfigurableApplicationContext context;
    @Shared
    boolean sharedSetupDone = false;
    @Autowired
    private DataSource ds;
    @Autowired
    private BookRepository bookRepository;
    @Autowired
    private PublisherRepository publisherRepository;
    @Shared
    private MockMvc mockMvc;

    void setup() {
        if (!sharedSetupDone) {
            mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
            sharedSetupDone = true;
        }
        ResourceDatabasePopulator populator = new 
               ResourceDatabasePopulator(context.getResource("classpath:/packt-books.sql"));
        DatabasePopulatorUtils.execute(populator, ds);
    }

    @Transactional
    def "Test RESTful GET"() {
        when:
        def result = mockMvc.perform(get("/books/${isbn}"));

        then:
        result.andExpect(status().isOk()) 
       result.andExpect(content().string(containsString(title)));

       where:
       isbn              | title
      "978-1-78398-478-7"|"Orchestrating Docker"
      "978-1-78528-415-1"|"Spring Boot Recipes"
    }

    @Transactional
    def "Insert another book"() {
      setup:
      def existingBook = bookRepository.findBookByIsbn("978-1-78528-415-1")
      def newBook = new Book("978-1-12345-678-9", "Some Future Book",
              existingBook.getAuthor(), existingBook.getPublisher())

      expect:
      bookRepository.count() == 3

      when:
      def savedBook = bookRepository.save(newBook)

      then:
      bookRepository.count() == 4
      savedBook.id > -1
  }
}
  • 执行测试用例,测试通过
  • 接下来试验下Spock如何与mock对象一起工作,之前的文章中我们已经在TestMockBeansConfig类中定义了PublisherRepository的Spring Bean,如下所示,由于@Primary的存在,使得在运行测试用例时Spring Boot优先使用Mockito框架模拟出的实例。
@Configuration
@UsedForTesting
public class TestMockBeansConfig {
    @Bean
    @Primary
    public PublisherRepository createMockPublisherRepository() {
        return Mockito.mock(PublisherRepository.class);
    }
}
  • 在BookController.java中添加getBooksByPublisher接口,代码如下所示:
@Autowired
public PublisherRepository publisherRepository;

@RequestMapping(value = "/publisher/{id}", method = RequestMethod.GET)
public List<Book> getBooksByPublisher(@PathVariable("id") Long id) {
    Publisher publisher = publisherRepository.findOne(id);
    Assert.notNull(publisher);
    return publisher.getBooks();
}
  • SpockBookRepositorySpecification.groovy文件中添加对应的测试用例,
def "Test RESTful GET books by publisher"() {
    setup:
    Publisher publisher = new Publisher("Strange Books")
    publisher.setId(999)
    Book book = new Book("978-1-98765-432-1",
            "Mytery Book",
            new Author("Jhon", "Done"),
            publisher)
    publisher.setBooks([book])
    Mockito.when(publisherRepository.count()).
            thenReturn(1L);
    Mockito.when(publisherRepository.findOne(1L)).
            thenReturn(publisher)

    when:
    def result = mockMvc.perform(get("/books/publisher/1"))

    then:
    result.andExpect(status().isOk())
    result.andExpect(content().string(containsString("Strange Books")))

    cleanup:
    Mockito.reset(publisherRepository)
}
  • 运行测试用例,发现可以测试通过,在控制器将对象转换成JSON字符串装入HTTP响应体时,依赖Jackson库执行转换,可能会有循环依赖的问题——在模型关系中,一本书依赖一个出版社,一个出版社有包含多本书,在执行转换时,如果不进行特殊处理,就会循环解析。我们这里通过@JsonBackReference注解阻止循环依赖。

分析

可以看出,通过Spock框架可以写出优雅而强大的测试代码。

首先看SpockBookRepositorySpecification.groovy文件,该类继承自Specification类,告诉JUnit这个类是测试类。查看Specification类的源码,可以发现它被@RunWith(Sputnik.class)注解修饰,这个注解是连接Spock与JUnit的桥梁。除了引导JUnit,Specification类还提供了很多测试方法和mocking支持。

Note:关于Spock的文档见这里:Spock Framework Reference Documentation

根据《单元测试的艺术》一书中提到的,单元测试包括:准备测试数据、执行待测试方法、判断执行结果三个步骤。Spock通过setup、expect、when和then等标签将这些步骤放在一个测试用例中。

  • setup:这个块用于定义变量、准备测试数据、构建mock对象等;
  • expect:一般跟在setup块后使用,包含一些assert语句,检查在setup块中准备好的测试环境
  • when:在这个块中调用要测试的方法;
  • then : 一般跟在when后使用,尽可以包含断言语句、异常检查语句等等,用于检查要测试的方法执行后结果是否符合预期;
  • cleanup:用于清除setup块中对环境做的修改,即将当前测试用例中的修改回滚,在这个例子中我们对publisherRepository对象执行重置操作。

Spock也提供了setup()和cleanup()方法,执行一些给所有测试用例使用的准备和清除动作,例如在这个例子中我们使用setup方法:(1)mock出web运行环境,可以接受http请求;(2)加载packt-books.sql文件,导入预定义的测试数据。web环境只需要Mock一次,因此使用sharedSetupDone这个标志来控制。

通过@Transactional注解可以实现事务操作,如果某个方法被该注解修饰,则与之相关的setup()方法、cleanup()方法都被定义在一个事务内执行操作:要么全部成功、要么回滚到初始状态。我们依靠这个方法保证数据库的整洁,也避免了每次输入相同的数据。



相关文章
|
25天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
19天前
|
Java 应用服务中间件
SpringBoot获取项目文件的绝对路径和相对路径
SpringBoot获取项目文件的绝对路径和相对路径
59 1
SpringBoot获取项目文件的绝对路径和相对路径
|
10天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
22 2
|
15天前
|
分布式计算 关系型数据库 MySQL
SpringBoot项目中mysql字段映射使用JSONObject和JSONArray类型
SpringBoot项目中mysql字段映射使用JSONObject和JSONArray类型 图像处理 光通信 分布式计算 算法语言 信息技术 计算机应用
36 8
|
21天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
40 2
|
20天前
|
消息中间件 NoSQL Java
springboot整合常用中间件框架案例
该项目是Spring Boot集成整合案例,涵盖多种中间件的使用示例,每个案例项目使用最小依赖,便于直接应用到自己的项目中。包括MyBatis、Redis、MongoDB、MQ、ES等的整合示例。
79 1
|
21天前
|
JavaScript 前端开发 Java
SpringBoot项目的html页面使用axios进行get post请求
SpringBoot项目的html页面使用axios进行get post请求
41 2
|
21天前
|
前端开发 Java Spring
SpringBoot项目thymeleaf页面支持词条国际化切换
SpringBoot项目thymeleaf页面支持词条国际化切换
51 2
|
21天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
34 1
|
24天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。