共计 6663 个字符,预计需要花费 17 分钟才能阅读完成。
本文首发于个人网站:在 Spring Boot 项目中使用 Spock 测试框架
Spock 框架是基于 Groovy 语言的测试框架,Groovy 与 Java 具备良好的互操作性,因此可以在 Spring Boot 项目中使用该框架写优雅、高效以及 DSL 化的测试用例。Spock 通过 @RunWith 注解与 JUnit 框架协同使用,另外,Spock 也可以和 Mockito(Spring Boot 应用的测试——Mockito)一起使用。
在这个小节中我们会利用 Spock、Mockito 一起编写一些测试用例(包括对 Controller 的测试和对 Repository 的测试),感受下 Spock 的使用。
实战
- 根据 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()方法都被定义在一个事务内执行操作:要么全部成功、要么回滚到初始状态。我们依靠这个方法保证数据库的整洁,也避免了每次输入相同的数据。
Spring Boot 1.x 系列
- Spring Boot 的自动配置、Command-line-Runner
- 了解 Spring Boot 的自动配置
- Spring Boot 的 @PropertySource 注解在整合 Redis 中的使用
- Spring Boot 项目中如何定制 HTTP 消息转换器
- Spring Boot 整合 Mongodb 提供 Restful 接口
- Spring 中 bean 的 scope
- Spring Boot 项目中使用事件派发器模式
- Spring Boot 提供 RESTful 接口时的错误处理实践
- Spring Boot 实战之定制自己的 starter
- Spring Boot 项目如何同时支持 HTTP 和 HTTPS 协议
- 自定义的 Spring Boot starter 如何设置自动配置注解
- Spring Boot 项目中使用 Mockito
本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。