为何会专门写这篇BDD呢?之前我发表过一篇《代码重构之TDD的思考》,有童靴联系到我,探讨之余,感觉这几年集成化方面的测试方案产出太少了,优秀的也太少了。今天带大家了解一个新东西“BDD”,纠结是新瓶换旧药还是别的呢?
BDD(Behavior-Driven Development),也叫行为驱动开发,感觉是不是像TDD(Test-Driven Development)一样的空白无力。其实不然,它是在TDD的基础上发展而来的一种软件开发方法。
首先我们剖析一下TDD的问题有哪些:
- 最大的弊端是面对一大堆的功能需求和用例时往往会感到无从下手
- TDD更侧重于测试本身,因此容易忽视对业务需求的表达,最终沉溺于琐碎细节而无法自拔
- 它没有相应的系统的解决方案(相当一部分人认为junit/testng+Jmock就行)
那BDD就能解决上面这些问题吗?个人认为,不完全能够,它只是提供了一直新的思路,应该说是一整套的解决方案(spock)。
战略视角看
传统的软件开发(总而言之就是各司其职,浓厚的部门墙)
BDD的本质在于尽可能避免在需求描述、用例撰写、代码实现、测试等各环节衔接、转译过程中发生的信息丢失。BDD以用例Use Case和示例Example为核心,借助Gherkin等一些特有概念和一系列BDD特有工具,以更贴近业务场景的方式,实现软件需求的完整实现。
战术层面
BDD的每个迭代可以按下面的方法来进行:
这里你会问Business Goal是个什么鬼?
采用Why-Who-How-What四步法发掘Business Goal
Why:出于何种原因要开发此系统?
Who:谁将从系统中受益?谁是系统的用户?谁会影响系统的开发?
How:怎样才能更便捷、更容易地达成业务目标?
What:系统能做哪些工作来实现业务目标?
发现Feature时,则采用了Why-Who-What-How四步法进行Feature的粒度细分,得到最终的用户故事。
- 规划整个系统,得到抽象的业务目标Business Goal
- 将业务目标细化为若干个具体的功能需求Feature
- 采取Given-When-Then三段式编写Gherkin,用具体的场景示例Example描述和演示特定的功能
- 将Example转换为可执行的Specification
- 将Specification再拆解为更低层次的、逼近代码实现的Low-Level Specification与测试Test
- 从Low-Level Specification中得到代码实现,并析取活动文档Living Documentation
最终整体的流程大概是这样子的:
对,它产出的也是测试代码,那和junit有啥区别啊?不急
与junit的区别
JUnit关注的则是更细碎的单元测试层面的东西。二者关注的角度、粒度和要解决的问题都不同。Spock能胜任从单元测试、集成测试到验收测试的所有工作。
怎样快速上手
这里稍微讲一下,在Spring Boot项目中使用Spock框架,其实也比较简单,它采用Gherkin语法形式(Given-When-Then),采用groovy语言来完成测试。编写一个测试用例主要会用到这些,
- setup:这个块用于定义变量、准备测试数据、构建mock对象等;
- expect:一般跟在setup块后使用,包含一些assert语句,检查在setup块中准备好的测试环境
- when:在这个块中调用要测试的方法;
- then : 一般跟在when后使用,尽可以包含断言语句、异常检查语句等等,用于检查要测试的方法执行后结果是否符合预期;
- cleanup:用于清除setup块中对环境做的修改,即将当前测试用例中的修改回滚,在这个例子中我们对publisherRepository对象执行重置操作。
注意:spring-boot-maven-plugin这个插件同时也支持在Spring Boot框架中使用Groovy语言。
<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>
/**
* step1:
* 在src/test目录下创建groovy文件夹,在groovy文件夹下创建com/test/bookpub包。
* 在resources目录下添加packt-books.sql文件,内容如下所示:
* INSERT INTO author(id, first_name, last_name) VALUES(2, 'alex', 'chen');
* INSERT INTO book(isbn, title, author, publisher) VALUES('9527', 'Docker', 2, 1);
* step2:
* 在com/test/bookpub目录下创建SpockBookRepositorySpecification.groovy文件
*/
@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
}
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如何与mock对象一起工作
@Configuration
@UsedForTesting
public class TestMockBeansConfig {
@Bean
@Primary
public PublisherRepository createMockPublisherRepository() {
return Mockito.mock(PublisherRepository.class);
}
}
//测试"Test RESTful GET books by publisher"
@Controller
public class BookController{
@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();
}
}
到这里基本上已经结束完了,最后一本必读书籍《Java Testing with Spock》 。