BDD敏捷開發入門與實戰

BDD敏捷開發入門與實戰


1.BDD的來由

2003年,Dan North首先提出了BDD的概念,並在隨後開發出了JBehave框架。在Dan North博客上介紹BDD的文章中,說到了BDD的想法是從何而來。簡略瞭解一下BDD的歷史和背景,有助於咱們更好地理解。java

1.1 TDD的困惑

Dan在使用TDD敏捷實踐時,時常會有不少一樣的困惑縈繞腦海,這也是不少程序員敏捷實踐都想知道的:node

  • 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」redis

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的文檔是如何與咱們程序員的代碼結合到一塊兒的呢?下一節咱們就詳細分析一下。markdown


2.三個核心概念

Feature、Scenario、Steps是BDD的三個核心概念,體現了BDD的三個重要價值:網絡

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

2.1 Feature

Feature就像是文檔同樣,描述了功能特性、角色、以及 最重要的商業價值app

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文件中各類場景對應的代碼,讓它變成「活文檔」!ide


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的粒度和抽象層次,並以統一而規範的語言做爲文檔,消除了軟件開發中各類人員的溝通障礙。同時以實用的框架將文檔與代碼粘合到一塊兒,使文檔可執行化、代碼文檔化。

相關文章
相關標籤/搜索