BDD敏捷开发入门与实战

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的粒度和抽象层次,并以统一而规范的语言作为文档,消除了软件开发中各种人员的沟通障碍。同时以实用的框架将文档与代码粘合到一起,使文档可执行化、代码文档化。

你可能感兴趣的:(敏捷开发)