Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目中使用该框架写优雅、高效以及DSL化的测试用例。Spock通过 @RunWith 注解与JUnit框架协同使用,另外,Spock也可以和Mockito( Spring Boot应用的测试——Mockito 一起使用。
在这个小节中我们会利用Spock、Mockito一起编写一些测试用例(包括对Controller的测试和对Repository的测试),感受下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>
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);
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 } }
@Configuration @UsedForTesting public class TestMockBeansConfig { @Bean @Primary public PublisherRepository createMockPublisherRepository() { return Mockito.mock(PublisherRepository.class); } }
@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(); }
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) }
可以看出,通过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等标签将这些步骤放在一个测试用例中。
Spock也提供了setup()和cleanup()方法,执行一些给所有测试用例使用的准备和清除动作,例如在这个例子中我们使用setup方法:(1)mock出web运行环境,可以接受http请求;(2)加载packt-books.sql文件,导入预定义的测试数据。web环境只需要Mock一次,因此使用sharedSetupDone这个标志来控制。
通过@Transactional注解可以实现事务操作,如果某个方法被该注解修饰,则与之相关的setup()方法、cleanup()方法都被定义在一个事务内执行操作:要么全部成功、要么回滚到初始状态。我们依靠这个方法保证数据库的整洁,也避免了每次输入相同的数据。
本号专注于后端技术、JVM问题排查和优化、Java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。