本文首发于个人网站: 在 Spring Boot 项目中使用 Spock 测试框架 http://www.javaadu.online/?p=588
Spock 框架是基于 Groovy 语言的测试框架, Groovy 与 Java 具备良好的互操作性, 因此可以在 Spring Boot 项目中使用该框架写优雅, 高效以及 DSL 化的测试用例. Spock 通过 @RunWith 注解与 JUnit 框架协同使用, 另外, Spock 也可以和 Mockito(Spring Boot 应用的测试 --Mockito http://www.javaadu.online/?p=575 ) 一起使用.
在这个小节中我们会利用 Spock,Mockito 一起编写一些测试用例 (包括对 Controller 的测试和对 Repository 的测试), 感受下 Spock 的使用.
实战
根据 Building an Application with Spring Boot https://spring.io/guides/gs/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 http://www.javaadu.online/?p=487
了解 Spring Boot 的自动配置 http://www.javaadu.online/?p=495
Spring Boot 的 @PropertySource 注解在整合 Redis 中的使用 http://www.javaadu.online/?p=499
Spring Boot 项目中如何定制 HTTP 消息转换器 http://www.javaadu.online/?p=515
Spring Boot 整合 MongoDB 提供 Restful 接口 http://www.javaadu.online/?p=518
Spring 中 bean 的 scope http://www.javaadu.online/?p=521
Spring Boot 项目中使用事件派发器模式 http://www.javaadu.online/?p=526
Spring Boot 提供 RESTful 接口时的错误处理实践 http://www.javaadu.online/?p=530
Spring Boot 实战之定制自己的 starter http://www.javaadu.online/?p=535
Spring Boot 项目如何同时支持 HTTP 和 HTTPS 协议 http://www.javaadu.online/?p=538
自定义的 Spring Boot starter 如何设置自动配置注解 http://www.javaadu.online/?p=546
Spring Boot 项目中使用 Mockito http://www.javaadu.online/?p=575
本号专注于后端技术, JVM 问题排查和优化, Java 面试题, 个人成长和自我管理等主题, 为读者提供一线开发者的工作和成长经验, 期待你能在这里有所收获.
来源: https://www.cnblogs.com/javaadu/p/11748473.html