BDD敏捷开发入门与实战


1.BDD的来由

2003年,Dan North首先提出了BDD的概念,并在随后开发出了JBehave框架。在Dan North博客上介绍BDD的文章中,说到了BDD的想法是从何而来。简略了解一下BDD的历史和背景,有助于我们更好地理解。

1.1 TDD的困惑

Dan在使用TDD敏捷实践时,时常会有很多同样的困惑萦绕脑海,这也是很多程序员敏捷实践都想知道的:

  • where to start
  • what to test
  • what not to test
  • how much to test in one go
  • what to call their tests
  • how to understand why a test fails

1.2 同事的小框架

当Dan用上了一位同事编写的小框架agiledox时,灵感闪现!这个框架其实很简单,它基于JUnit测试框架,根据测试类名和方法名,将每个测试方法都打印为类似文档的输出。程序员们意识到这个小玩具可以帮它们做一些文档性的工作,于是就开始用商业领域语法命名他们的类和方法,让agiledox产生的输出能直接被商业客户、分析师、测试人员都看懂!

// CustomerLookup
// - finds customer by id
// - fails for duplicate customers
// - ...
public class CustomerLookupTest extends TestCase {
testFindsCustomerById() {
...
}
testFailsForDuplicateCustomers() {
...
}
...
}

1.3 “Ubiquitous Language”

此时,恰逢Eric Evans发表了畅销书DDD(领域驱动设计),其中描述了为系统建模时,使用一种基于商业领域模型的Ubiquitous Language,让业务词汇渗透到代码中。于是,Dan决定定义一种分析师、测试人员、开发者、业务人员、用户都能懂的”Ubiquitous Language”

Feature: <description>
As a <role>
I want <feature>
So that <business value> Scenario: <description>
Given <some initial context>,
When <an event occurs>,
Then <ensure some outcomes>.

就这样,BDD的雏形就出现了!但这种类似BRD的文档是如何与我们程序员的代码结合到一起的呢?下一节我们就详细分析一下。


2.三个核心概念

Feature、Scenario、Steps是BDD的三个核心概念,体现了BDD的三个重要价值:

  • Living Document
  • Executable Specification by Example(SbE)
  • Automated Tests

2.1 Feature

Feature就像是文档一样,描述了功能特性、角色、以及 最重要的商业价值

2.2 Scenario

场景就是上面提到的规范Specification。Cucumber提供了Scenario、Scenario Outline两种形式。使用时要注意,在Cucumber官博上的一篇文章“Are you doing BDD? Or are you just using Cucumber?”给出了一个反模式。

Scenario Outline: Detect agent type based on contract number (single contract found)
Given I am on the "Find me" page
And I have entered a contract number
When I click the "Continue" button
And a contract number match is found
And the agent type is <DistributorType>
Then the contract number field will become uneditable
And the "Back" button will be displayed
And the following <text> and <input field type> will be displayed Examples:
| DistributorType | input field type | text |
| Broker | Date of birth | Please enter your last name |
| TiedAgent | Last name | Please enter your date of birth |

看出来了差别吧:Scenario Outline的核心依然应该是商业规则,而不能因为它对输入和输出的细化就将重点转移到UI界面

Scenario: Customer has a broker policy so DOB is requested
Given I have a "Broker" policy
When I submit my policy number
Then I should be asked for my date of birth Scenario: Customer has a tied agent policy so last name is requested
Given I have a "TiedAgent" policy
When I submit my policy number
Then I should be asked for my last name

2.3 Steps

Steps就是实际编码了,我们要在Java中实现出Feature文件中各种场景对应的代码,让它变成“活文档”!


3.实战(上):分布式集群构建

之所以选择这么一个例子来实战,是因为网上的大部分例子都很简单而且雷同。通过这个例子,也是想试验一下BDD对于“业务性”不强的而且还是分布式的系统(即基础设施或中间件)是否也能发挥作用。这次实战也是一次比较奇妙的经历,不少核心类、接口和关于系统设计的想法都在这个过程中自然涌现~

3.1 开发环境

IDE当然还是选择Intellij,并且开启Cucumber插件,因为本实例是基于Cucumber实现的(其实其他的框架如JBehave都非常类似)。然后新建Maven工程,引入以下依赖包:

    <dependencies>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-java</artifactId>
<version>1.2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>1.2.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

3.2 编写feature文件

Feature相对比较好写,简单描述一下功能特性就行了。比如下面的集群自动创建功能:为了自动创建集群(功能),作为用户(角色),我想结点能自动互相发现形成集群以节省手工的工作量和时间(商业价值)。

Feature: Auto Cluster Creation
In order to create a cluster automatically
As a user
I want the nodes can discover each other on their own

我们还需要一个启动类:

@RunWith(Cucumber.class)
@CucumberOptions(plugin={"pretty"}, features="src/test/resources", tags = {})
public class NodeDiscoveryStory {
}

3.3 选择典型场景

为了简化,我只选了一个最简单的两结点集群建立的场景。首先结点1形成集群A,当结点2加入集群A后,集群中应有两个结点1和2。

Scenario: create a cluster
Given node-1 in cluster-A starts
When a new node-2 in cluster-A starts
Then cluster-A should have node: 1,2

场景的选择和编写至关重要,本例的实践过程中就碰到了一些问题,下面做一点个人的经验总结:

  • Given和When不要混淆:一个是环境上下文,一个是触发条件,例如”a cluster is running”和”a new node starts”。弄混的结果就是在场景1里的Given在2里又原封不动的变成When了。
  • 场景是可验证的不能含糊:这一点上与Feature不一样。一开始我描述的场景就比较模糊不清,例如”Then the cluster can acknowledge the new node”,这种描述不够精确,不好验证对错。实际上仔细想想,BDD对应设计的高层次与行为结果的可验证是不矛盾的
  • 只选几个典型场景:在BDD中千万不要追求覆盖率和细粒度,否则就将丧失BDD对业务逻辑的表现力!在Feature文件里只描述最核心的东西,把覆盖率这种只有我们程序员和QA关心的东西隐藏起来,在更细粒度的Case中去完成。

此外,还有关于Given和When是否要细分出一些And条件,比如本例中的Given和When就都可以分别拆成createNode和createOrJoinCluster两步,但这样的话会导致成员变量增多而显得比较乱,因为Cucumber中的Given和And、When和And之间是不能携带过去对象的。所以从下一部分的编码实现中能看出,最终我还是没有拆的那么细。

3.4 Steps编码实现

编码实现是最痛苦也最有收获的!一开始时一无所有的茫然,不断重构最终终于找到比较合理的设计。注意:代码不要跟着场景的描述走,比如变量cluster起名为clusterA,那就限定死了!我们的Steps应该是通用的,这里的Given、When都是可能用于其他场景的。

首先在@Given中启动一个Cluster加入一个Node,之后在@When中模拟在另一台机器上启动一个Node加入到集群的过程。因为实际上这个过程是在远程完成的,所以不能直接使用成员变量cluster。最后验证cluster中的结点列表,看是否已经包含两个结点。

public class MyStepdefs {

    private Cluster cluster;

    @Given("^node-(\\w+) in cluster-(\\w+) starts$")
public void runCluster(String nodeId, String clusterName) {
Node node = new Node(nodeId);
cluster = new Cluster(clusterName, new CoordinatorMock());
node.join(cluster);
} @When("^a new node-(\\w+) in cluster-(\\w+) starts$")
public void startNewNodeToJoinCluster(String nodeId, String clusterName) {
Node newNode = new Node(nodeId);
Cluster clusterSlave = new Cluster(clusterName, new CoordinatorMock());
newNode.join(clusterSlave);
} @Then("^cluster-(\\w+) should have node: (.+)$")
public void joinCluster(String clusterName, List<String> nodeIds) {
Assert.assertEquals(clusterName, cluster.getName()); List<String> actualNodeIds = new ArrayList<String>();
for (Node node : cluster.getNodes()) {
actualNodeIds.add(node.getId());
}
Collections.sort(actualNodeIds);
Assert.assertEquals(nodeIds, actualNodeIds);
} }

对比下面典型的单元测试代码能够看出,BDD的Steps代码因为对应着Scenario,所以步骤分的比较清楚。而在普通Test Case中,Case中就会堆砌着类似@Given、@When、@Then的代码,并且每个Case都会有类似的代码。所以一般我们会提取出一些公关的代码,以使Case更为清晰,但BDD则直接更进一步。

    @Test
public void testCachePut2_List() throws Exception {
CacheResult<Object> ret = redis.cachePut(CACHE_NAME,
Arrays.asList(
new Person(1, 49, "alan"),
new Person(2, 34, "hank"),
new Person(3, 38, "carter")
)
);
Assert.assertTrue(ret.isOK()); List persons = redis.cacheGetAll(CACHE_NAME, Arrays.asList(1, 3)).getValue();
Collections.sort(persons); Assert.assertNotNull(persons);
Assert.assertEquals(2, persons.size()); ...
}

4.实战(下):核心类进化

下面就说一下通过这次BDD历险得到的核心类,以及是如何思考出来的。这个重构、思考、最终浮现出来的过程其实是最重要的!

最先映入脑海的就是Cluster和Node,其实Node也可以暂时用一个ID代替,之后有需要时再抽象成类,这里有些“着急”了直接新建了个Node类。

public class Cluster {

    private final String name;

    private List<Node> nodes = new ArrayList<Node>();

    public Cluster(String name) {
this.name = name;
} public void addNode(Node node) {
} public String getName() {
return name;
} public List<Node> getNodes() {
return nodes;
}
} public class Node { private String nodeId; public Node(String nodeId) {
this.nodeId = nodeId;
} public void join(Cluster cluster) {
cluster.addNode(this);
} public String getId() {
return nodeId;
}
}

写好了@Given、@When、@Then之后,就可以跑起来Cucumber试试了,肯定是报错的。现在自然就有疑问了,@Then中的断言如何能够成功呢?所以Cluster背后需要一个能够帮助分布式通信的组件,于是就加上Coordinator接口。同时,我们创建一个Mock实现,利用static静态变量模拟网络通信的过程。

public interface Coordinator {

    void register(Cluster cluster);

    boolean addNode(Node node);

}

public class CoordinatorMock implements Coordinator {

    /** Simulate network communication */
private static List<Cluster> clusters = new ArrayList<Cluster>(); @Override
public void register(Cluster cluster) {
clusters.add(cluster);
} @Override
public boolean addNode(Node node) {
for (Cluster cluster : clusters) {
cluster.handleAddNode(node);
}
return true;
}
}

最后让Cluster注册到Coordinator上,调用addNode()接口模拟分布式通信,并添加handleAddNode()处理请求就可以了!这样我们就完成了BDD的一个简单实例!

public class Cluster {

    private final String name;

    private final Coordinator coordinator;

    private List<Node> nodes = new ArrayList<Node>();

    public Cluster(String name, Coordinator coordinator) {
this.name = name;
this.coordinator = coordinator; coordinator.register(this);
} public void addNode(Node node) {
coordinator.addNode(node);
} public void handleAddNode(Node node) {
nodes.add(node);
} public String getName() {
return name;
} public List<Node> getNodes() {
return nodes;
}
}

5.总结

每种新事物的产生都不可避免地会伴随着各种各样的解读,毕竟每个人都有自己的看法和理解。有的理解深刻直达本质,有的独辟蹊径另立门派,也有的是偏见和误解。BDD也一样,可能会人被当做跟TDD一样的东西,也可能会被看做测试的一种。

通过本文的介绍,大家应该能看到BDD的闪光点。它提升了TDD的粒度和抽象层次,并以统一而规范的语言作为文档,消除了软件开发中各种人员的沟通障碍。同时以实用的框架将文档与代码粘合到一起,使文档可执行化、代码文档化。

BDD敏捷开发入门与实战的更多相关文章

  1. Python开发入门与实战12-业务逻辑层

    12. Biz业务层 前面的章节我们把大量的业务函数都放在了views.py里,按照目前这一的写法,当我们编写的系统复杂较高时,我们的views.py将会越来越复杂,大量的业务函数包含其中使其成为一个 ...

  2. Python开发入门与实战20-微信开发配置

    随着移动互联网时代的来临,微信已经成为移动互联网移动端的主要入口,现在很多的大型企业都有自己的微信服务号,如:银行业有自己的微银行,基金公司的公众服务号.通过微信入口可以方便快速的实现企业提供的服务. ...

  3. Python开发入门与实战14-基于Extjs的界面

    14. 基于Extjs的界面 上一章我们实现了一个原生的html例子,本章我们将采用Extjs实现界面的展现,来说明MVC模式下我们是怎么考虑界面与业务层的关系的. 14.1. 引用Extjs目录 首 ...

  4. Python开发入门与实战13-基于模板的界面

    13. 基于模板的界面 本章我们将继续基于库存的简单例子来阐述如何在python django中体现MVC的架构,根据djangobook说明: M:数据存取部分,由django数据库层处理,本章要讲 ...

  5. Python开发入门与实战10-事务

    1. 事务 本章我们将通过一个例子来简要的说明“事务”,这个开发实战里经常遇到的名词.事务是如何体现在一个具体的业务和系统的实现里. 事务是通过将一组相关操作组合为一个,要么全部成功要么全部失败的单元 ...

  6. Python开发入门与实战2-第一个Django项目

    2.第一个Django项目 上一章节我们完成了python,django和数据库等运行环境的安装,现在我们来创建第一个django project吧,迈出使用django开发应用的第一步. 2.1.创 ...

  7. Python开发入门与实战1-开发环境

    1.搭建Python Django开发环境 1.1.Python运行环境安装 Python官网:http://www.python.org/ Python最新源码,二进制文档,新闻资讯等可以在Pyth ...

  8. Python开发入门与实战22-简单消息回复

    22. 简单消息回复 本章节我们来实现一个微信库存查询功能,使用我们前面的BIZ业务逻辑层示例如何利用微信入口来实现文本消息类的库存查询服务. 22.1. 在responseMsg函数里增加处理微信文 ...

  9. Python开发入门与实战19-Windows Azure web 应用部署

    19. 微软云web应用部署 上一章节我们介绍了如何实现在微软云通过虚拟机部署我们的在python django应用,本章我们来介绍如何Windows Azure上部署通过部署网站的方式来部署我们的应 ...

随机推荐

  1. Delphi X10.2 + FireDAC 使用 SQL 语句 INSERT

    // CREATE TABLE [tabusers]( // [id] INTEGER PRIMARY KEY AUTOINCREMENT, // [username] CHAR NOT NULL, ...

  2. ASP.NET CORE系列【五】webapi整理以及RESTful风格化

    介绍 什么是RESTful?  这里不多做赘述,详情请百度! 哈哈,本来还想巴拉巴拉介绍一些webapi, RESTful的, 还是算了,咱们直接上干货!(原因是懒!哈哈) 使用 以前使用过mvc的人 ...

  3. Override与Overload

    方法重写(Override) 方法重写是子类对父类(父类为抽象类)的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 方法的重写规则 1.参数列表必须完全与被重写 ...

  4. Linux64位程序中的漏洞利用

    之前在栈溢出漏洞的利用和缓解中介绍了栈溢出漏洞和一些常见的漏洞缓解 技术的原理和绕过方法, 不过当时主要针对32位程序(ELF32). 秉承着能用就不改的态度, IPv4还依然是互联网的主导, 更何况 ...

  5. 【MySQL】通过Binary Log简单实现数据回滚(一)

    一.前言 对,没错,我又水了好一阵子,深刻反思寄几.前段时间,工作项目上出于对excel等批量操作可能出现误操作的问题,要求提供一个能够根据操作批次进行数据回滚的能力.在开发的过程中接触到了MySQL ...

  6. 理解error和exception之间的区别

    很多程序员不清楚error和exception之间的区别,这区别对于如何正确的处理问题而言非常重要(见附1,"简要的叙述error和exception").就像Mary Campi ...

  7. [C#]设计模式-工厂方法-创建型模式

    介绍完简单工厂之后,现在就来介绍一下工厂方法模式.之前简单工厂有一个弊端就是每次你需要产生新的产品的时候都需要对创建工厂类进行更改,如同前一篇文章说过的一样最后的后果是工厂类会变得十分庞大. 而工厂方 ...

  8. 【django小练习之主机管理界面】

    需求: 利用django,js,bootstrap等实现登录,主机管理等操作. 实现截图 登录界面 主机界面,添加及编辑 部门管理界面 代码实现 目录层级 settings.py "&quo ...

  9. [NOI 2007]社交网络

    Description 在社交网络(socialnetwork)的研究中,我们常常使用图论概念去解释一些社会现象.不妨看这样的一个问题.在一个社交圈子里有n个人,人与人之间有不同程度的关系.我们将这个 ...

  10. 洛谷mNOIP模拟赛Day1-斐波那契

    题目背景 大样例下发链接:http://pan.baidu.com/s/1c0LbQ2 密码:jigg 题目描述 小 C 养了一些很可爱的兔子. 有一天,小 C 突然发现兔子们都是严格按照伟大的数学家 ...