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

  • 相关阅读:
    快速入门各种跨域
    常用知识点
    比较少用的格式
    git
    “没有用var声明的为全局变量”这种说法不准确
    类数组对象
    函数上下文的变量对象实例
    var a =10 与 a = 10的区别
    原型链与作用域链、执行上下文
    闭包的作用
  • 原文地址:https://www.cnblogs.com/xiaomaohai/p/6157589.html
Copyright © 2020-2023  润新知