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. spark2.1:rdd.combineByKeyWithClassTag的用法示例

    测试spark版本: Spark context Web UI available at http://192.168.1.1:32735 Spark context available as 'sc ...

  2. Java-Maven(八):IDEA使用本地maven,并配置远程中央仓库

    声明:已经安装了maven,安装请参考:<Java-Maven(一):Maven的简介与安装> 1)一般我们从github.码云(https://gitee.com)上获取代码后,实际上我 ...

  3. Java-Maven(三):Maven坐标、Maven仓库、Maven生命周期

    之前通过学习对maven命令有了部分了解,但是只是知道maven是用来做什么.但到目前位置还不足以全面的了解maven,为更全面的了解maven,需要学习以下几个概念:Maven坐标.Maven仓库. ...

  4. Java:List集合内的对象进行排序

    List集合中的对象进行排序,除了for外,还有java的Collections对象来对摸个集合进行排序的用法. 比如说我有一个List集合,集合元素为: public class TaskAutoE ...

  5. POJ-1122 FDNY to the Rescue!---Dijkstra+反向建图

    题目链接: https://vjudge.net/problem/POJ-1122 题目大意: 给出矩阵,矩阵中每个元素tij表示从第i个交叉路口到第j个交叉路口所需时间,若tij为-1则表示两交叉路 ...

  6. LabelFrame

    LabelFrame组件是Frame组件的变体. 默认情况下,LabelFrame会在其子组件的周围绘制一个边框以及一个标题. 何时使用LabelFrame组件?当你想要奖一些相关的组件分为一组的时候 ...

  7. 解析spring中的BeanFactory

    我们常把spring看作一个bean工厂或者ioc容器,它帮助我们负责对象的创建管理,以及对象间依赖关系的建立,还有其他的功能. 关于工厂的实现,一般来说与我们接触最多的就是BeanFactory和A ...

  8. 我花了 8 小时,"掌握"了一下 Flutter | Flutter 中文站上线

    Hi,大家好,我是承香墨影! 距离 Google 在 2018 世界移动大会上发布 Flutter 的 Beta 版本,Flutter 是 Google 用以帮助开发者在 Android 和 iOS ...

  9. 从零开始搭建支持http2的web服务

    前段时间开始,公司各项业务开始陆续接入http2,关于http2的优点与所适用的场景网上有很多的文档可以查阅,这里我主要是总结分享一下如何从0到1搭建http2服务. 这里先说明一下,要完成http2 ...

  10. php array_multisort函数实现按某一字段对二维数组进行排序

    在工作中碰到一个页面如表格似的展示多条数据,要求根据其中的修改时间对数据进行排序, 数据格式类似于 $a = array( 0=>array( editTime=>'' addTime=& ...