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的使用。
<!-- 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>
@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框架可以寫出優(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)簽將這些步驟放在一個測試用例中。
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ù)。