2003年,Dan North首先提出了BDD的概念,並在隨後開發出了JBehave框架。在Dan North博客上介紹BDD的文章中,說到了BDD的想法是從何而來。簡略瞭解一下BDD的歷史和背景,有助於咱們更好地理解。java
Dan在使用TDD敏捷實踐時,時常會有不少一樣的困惑縈繞腦海,這也是不少程序員敏捷實踐都想知道的:node
當Dan用上了一位同事編寫的小框架agiledox時,靈感閃現!這個框架其實很簡單,它基於JUnit測試框架,根據測試類名和方法名,將每一個測試方法都打印爲相似文檔的輸出。程序員們意識到這個小玩具能夠幫它們作一些文檔性的工做,因而就開始用商業領域語法命名他們的類和方法,讓agiledox產生的輸出能直接被商業客戶、分析師、測試人員都看懂!程序員
// CustomerLookup
// - finds customer by id
// - fails for duplicate customers
// - ...
public class CustomerLookupTest extends TestCase {
testFindsCustomerById() {
...
}
testFailsForDuplicateCustomers() {
...
}
...
}
此時,恰逢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
Feature、Scenario、Steps是BDD的三個核心概念,體現了BDD的三個重要價值:網絡
Feature就像是文檔同樣,描述了功能特性、角色、以及 最重要的商業價值。app
場景就是上面提到的規範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
Steps就是實際編碼了,咱們要在Java中實現出Feature文件中各類場景對應的代碼,讓它變成「活文檔」!ide
之因此選擇這麼一個例子來實戰,是由於網上的大部分例子都很簡單並且雷同。經過這個例子,也是想試驗一下BDD對於「業務性」不強的並且仍是分佈式的系統(即基礎設施或中間件)是否也能發揮做用。此次實戰也是一次比較奇妙的經歷,很多核心類、接口和關於系統設計的想法都在這個過程當中天然涌現~
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>
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 {
}
爲了簡化,我只選了一個最簡單的兩結點集羣創建的場景。首先結點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是否要細分出一些And條件,好比本例中的Given和When就均可以分別拆成createNode和createOrJoinCluster兩步,但這樣的話會致使成員變量增多而顯得比較亂,由於Cucumber中的Given和And、When和And之間是不能攜帶過去對象的。因此從下一部分的編碼實現中能看出,最終我仍是沒有拆的那麼細。
編碼實現是最痛苦也最有收穫的!一開始時一無全部的茫然,不斷重構最終終於找到比較合理的設計。注意:代碼不要跟着場景的描述走,好比變量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());
...
}
下面就說一下經過此次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;
}
}
每種新事物的產生都不可避免地會伴隨着各類各樣的解讀,畢竟每一個人都有本身的見解和理解。有的理解深入直達本質,有的獨闢蹊徑另立門派,也有的是偏見和誤解。BDD也同樣,可能會人被當作跟TDD同樣的東西,也可能會被看作測試的一種。
經過本文的介紹,你們應該能看到BDD的閃光點。它提高了TDD的粒度和抽象層次,並以統一而規範的語言做爲文檔,消除了軟件開發中各類人員的溝通障礙。同時以實用的框架將文檔與代碼粘合到一塊兒,使文檔可執行化、代碼文檔化。