淘先锋技术网

首页 1 2 3 4 5 6 7

为何会专门写这篇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的粒度细分,得到最终的用户故事。

  1. 规划整个系统,得到抽象的业务目标Business Goal
  2. 将业务目标细化为若干个具体的功能需求Feature
  3. 采取Given-When-Then三段式编写Gherkin,用具体的场景示例Example描述和演示特定的功能
  4. 将Example转换为可执行的Specification
  5. 将Specification再拆解为更低层次的、逼近代码实现的Low-Level Specification与测试Test
  6. 从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》 。