美文网首页SpringBootSpring Boot程序员
在Spring Boot项目中使用Spock框架

在Spring Boot项目中使用Spock框架

作者: 程序熊大 | 来源:发表于2015-12-22 16:59 被阅读6900次

    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()方法都被定义在一个事务内执行操作:要么全部成功、要么回滚到初始状态。我们依靠这个方法保证数据库的整洁,也避免了每次输入相同的数据。

    相关文章

      网友评论

      • 0ce13206290e:请问测试service层代码怎么测试?能否写篇文章指引下
        0ce13206290e:@杜琪 我是想知道Spock框架如何在service层进行单元测试,依赖注入的类 都是NULL,test模块下扫描不到声明的组件。
        程序熊大:http://www.jianshu.com/p/a8e17afd8c90
        :smile:
      • 0ce13206290e:springboot什么版本啊?SpringApplicationContextLoader 这个注解我怎么没有?1.5.7版本
        程序熊大:我的版本比较低了,有点记不清了
      • 大东胖了:是每个测试方法都需要加 setup: 么?
      • 程序熊大:(1)packt-bootks.sql文件的内容已经更新补上;(2)这里确实是我写错了,已经更正;(3)我之前是右键跑这个单元测试的,没有你考虑的这么全面,赞
      • dfd456b6587b:在创建完SpockBookRepositorySpecification.groovy文件 运行test的时候发现三个疑问
        1。麻烦提供下packt-books.sql。 我是根据代码里的"978-1-78398-478-7"|"Orchestrating Docker"自己随便创建了个,不知道对不对。
        2。containsString("title") 应该是containsString(title)吧?貌似多了双引号
        3。mvn clean test的时候并不能运行groovy文件,是我命令用的不对还是有其他的
        程序熊大:@chuanshaoye 是的,你这样联系起来看很好,不过我最开始的想法是每个测试用例用自己的数据
        dfd456b6587b:@杜琪 还有个问题 "978-1-78528-415-1"|"Spring Boot Recipes" 这里的isbn和你之前的文章里的对不上,应该是9876-5432-1111,以及下面的existingBook 的isbn也应该是这个值才能和之前的文章配合上。要不报错的 :blush:
        dfd456b6587b:@杜琪 3。添加gmavenplus-plugin并配置了maven-surefire-plugin, groovy test可以运行了,不过你文章里面说spring-boot-maven-plugin是支持groovy的,我也看了官方文章,但并没有找到怎么来运行test的
      • moxun:高产似母猪 :clap:
      • 曾樑::smile::smile:
        程序熊大:@曾樑 这篇实践性很强吧,groovy真是好东西

      本文标题:在Spring Boot项目中使用Spock框架

      本文链接:https://www.haomeiwen.com/subject/ywbmhttx.html