在Spring Boot項目中使用Spock測試框架

本文首發於個人網站:http://www.javaadu.online/?p=588

在Spring Boot項目中使用Spock測試框架

Spock框架是基於Groovy語言的測試框架,Groovy與Java具備良好的互操作性,因此可以在Spring Boot項目中使用該框架寫優雅、高效以及DSL化的測試用例。Spock通過@RunWith註解與JUnit框架協同使用,另外,Spock也可以和Mockito(http://www.javaadu.online/?p=575)一起使用。

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

實戰

  • 根據https://spring.io/guides/gs/spring-boot/這篇文章的描述,spring-boot-maven-plugin這個插件同時也支持在Spring Boot框架中使用Groovy語言。
  • 在pom文件中添加Spock框架的依賴

<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();
}
/<book>
  • 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的文檔見這裡:http://spockframework.github.io/spock/docs/1.0/index.html

根據《單元測試的藝術》一書中提到的,單元測試包括:準備測試數據、執行待測試方法、判斷執行結果三個步驟。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系列

  1. http://www.javaadu.online/?p=487
  2. http://www.javaadu.online/?p=495
  3. http://www.javaadu.online/?p=499
  4. http://www.javaadu.online/?p=515
  5. http://www.javaadu.online/?p=518
  6. http://www.javaadu.online/?p=521
  7. http://www.javaadu.online/?p=526
  8. http://www.javaadu.online/?p=530
  9. http://www.javaadu.online/?p=535
  10. http://www.javaadu.online/?p=538
  11. http://www.javaadu.online/?p=546
  12. http://www.javaadu.online/?p=575

本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。


分享到:


相關文章: