鍍金池/ 教程/ Java/ 在Spring Boot項(xiàng)目中使用Spock框架
通過JMX監(jiān)控Spring Boot應(yīng)用
Spring Boot:定制PropertyEditors
配置是否初始化Bean的方法
Spring Boot的自動配置、Command-line Runner
Spring Boot:定制URL匹配規(guī)則
Spring Boot的自動配置、Command-line Runner
利用Mockito模擬DB
Spring Boot應(yīng)用的打包和部署
了解Spring Boot的自動配置
Spring Boot應(yīng)用的健康監(jiān)控
了解Spring Boot的自動配置
初始化數(shù)據(jù)庫和導(dǎo)入數(shù)據(jù)
Spring Boot應(yīng)用的健康監(jiān)控
Docker with Spring Boot
RESTful by Spring Boot with MySQL
Spring Boot:定制攔截器
Spring Boot:定制static path mappings
Spring Boot with Mysql
Spring Boot:定制自己的starter
在測試中使用內(nèi)存數(shù)據(jù)庫
Restful: Spring Boot with Mongodb
Spring Boot with Redis
Spring Boot:定制HTTP消息轉(zhuǎn)換器
Spring Boot: Data Rest Service
Spring Boot:定制type Formatters
在Spring Boot項(xiàng)目中使用Spock框架
選擇Spring Boot項(xiàng)目的內(nèi)嵌容器
通過EmbeddedServletContainerCustomizer接口調(diào)優(yōu)Tomcat
Spring Boot應(yīng)用的打包和部署
Spring Boot Admin的使用
讓你的Spring Boot工程支持HTTP和HTTPS
Spring Boot:定制servlet filters
Spring Boot:定制URL匹配規(guī)則
Spring Boot應(yīng)用的測試——Mockito
Spring Boot應(yīng)用的測試——Mockito
Spring Boot:定制servlet filters
通過@Enable*注解觸發(fā)Spring Boot配置

在Spring Boot項(xiàng)目中使用Spock框架

Spock框架是基于Groovy語言的測試框架,Groovy與Java具備良好的互操作性,因此可以在Spring Boot項(xiàng)目中使用該框架寫優(yōu)雅、高效以及DSL化的測試用例。Spock通過@RunWith注解與JUnit框架協(xié)同使用,另外,Spock也可以和Mockito(Spring Boot應(yīng)用的測試——Mockito)協(xié)同使用。

在這個小節(jié)中我們會利用Spock、Mockito一起編寫一些測試用例(包括對Controller的測試和對Repository的測試),感受下Spock的使用。

How Do

  • 根據(jù)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目錄下創(chuàng)建groovy文件夾,在groovy文件夾下創(chuàng)建com/test/bookpub包。
  • com/test/bookpub目錄下創(chuàng)建SpockBookRepositorySpecification.groovy文件,內(nèi)容是:
@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
  }
}
  • 執(zhí)行測試用例,測試通過
  • 接下來試驗(yàn)下Spock如何與mock對象一起工作,之前的文章中我們已經(jīng)在TestMockBeansConfig類中定義了PublisherRepository的Spring Bean,如下所示,由于@Primary的存在,使得在運(yùn)行測試用例時Spring Boot優(yōu)先使用Mockito框架模擬出的實(shí)例。
@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文件中添加對應(yīng)的測試用例,
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)
}
  • 運(yùn)行測試用例,發(fā)現(xiàn)可以測試通過,在控制器將對象轉(zhuǎn)換成JSON字符串裝入HTTP響應(yīng)體時,依賴Jackson庫執(zhí)行轉(zhuǎn)換,可能會有循環(huán)依賴的問題——在模型關(guān)系中,一本書依賴一個出版社,一個出版社有包含多本書,在執(zhí)行轉(zhuǎn)換時,如果不進(jìn)行特殊處理,就會循環(huán)解析。我們這里通過@JsonBackReference注解阻止循環(huán)依賴。

分析

可以看出,通過Spock框架可以寫出優(yōu)雅而強(qiáng)大的測試代碼。

首先看SpockBookRepositorySpecification.groovy文件,該類繼承自Specification類,告訴JUnit這個類是測試類。查看Specification類的源碼,可以發(fā)現(xiàn)它被@RunWith(Sputnik.class)注解修飾,這個注解是連接Spock與JUnit的橋梁。除了引導(dǎo)JUnit,Specification類還提供了很多測試方法和mocking支持。

Note:關(guān)于Spock的文檔見這里:Spock Framework Reference Documentation

根據(jù)《單元測試的藝術(shù)》一書中提到的,單元測試包括:準(zhǔn)備測試數(shù)據(jù)、執(zhí)行待測試方法、判斷執(zhí)行結(jié)果三個步驟。Spock通過setup、expect、when和then等標(biāo)簽將這些步驟放在一個測試用例中。

  • setup:這個塊用于定義變量、準(zhǔn)備測試數(shù)據(jù)、構(gòu)建mock對象等;
  • expect:一般跟在setup塊后使用,包含一些assert語句,檢查在setup塊中準(zhǔn)備好的測試環(huán)境
  • when:在這個塊中調(diào)用要測試的方法;
  • then : 一般跟在when后使用,盡可以包含斷言語句、異常檢查語句等等,用于檢查要測試的方法執(zhí)行后結(jié)果是否符合預(yù)期;
  • cleanup:用于清除setup塊中對環(huán)境做的修改,即將當(dāng)前測試用例中的修改回滾,在這個例子中我們對publisherRepository對象執(zhí)行重置操作。

Spock也提供了setup()和cleanup()方法,執(zhí)行一些給所有測試用例使用的準(zhǔn)備和清除動作,例如在這個例子中我們使用setup方法:(1)mock出web運(yùn)行環(huán)境,可以接受http請求;(2)加載packt-books.sql文件,導(dǎo)入預(yù)定義的測試數(shù)據(jù)。web環(huán)境只需要Mock一次,因此使用sharedSetupDone這個標(biāo)志來控制。

通過@Transactional注解可以實(shí)現(xiàn)事務(wù)操作,如果某個方法被該注解修飾,則與之相關(guān)的setup()方法、cleanup()方法都被定義在一個事務(wù)內(nèi)執(zhí)行操作:要么全部成功、要么回滾到初始狀態(tài)。我們依靠這個方法保證數(shù)據(jù)庫的整潔,也避免了每次輸入相同的數(shù)據(jù)。