BDD

行為導向開發 (Behavior Driven Development, BDD)

2019/03/30 (新增內容)

2020/05/03 (新增連結 & 調整內容)

2020/06/09 (新增連結)

行為導向開發(Behavior Driven Development, BDD)是建構在Test Driven Development (TDD)之上,強調要以可測試的驗收條件 (Testable Acceptance Criteria)為撰寫需求的方式。BDD強調利用Given, When, Then描述不同情境下的需求,這個概念進一步可將需求轉換成可測試的驗收條件。如此一來,也更容易在系統開發前就已經準備好如何測試,也更容易將測試自動化(詳參: 3 Ways How Specification By Example and Gherkin Improve Collaboration)。BDD可以使用Gherkins(語言)來描述需求,使用Cucumber (或其他的工具)來轉換以Gherkins所寫的需求成為test case。

  • Behaviour Driven Development in Agile — a Step by Step Guide to start

    • Phases of BDD in Agile

      • Discovery of Behaviour Driven Development in Agile

      • Formulation of BDD in Agile

      • Automation in Behaviour Driven Development in Agile

  • Beyond BDD Best Practices

    • Define the actor — declare persona with attributes.

    • Be clear on the goal of the scenario — the scenario should have a concise title, a task that triggers an outcome, and an outcome.

    • Use concrete and realistic examples — but not too specific if no control over changing data.

    • One test per scenario — with exceptions such as when it takes too much time to reproduce the scenario.

    • Use application domain language — implementation is up to creativity. Use deep linking, API queries, SQL queries, whatever that works without sacrificing the writing style.

很多人會認為,開發的時間都來不及了,怎麼可能先準備好測試案例? 以BDD+TDD的概念,就是在寫需求的時候,就開始準備測試案例,也利用撰寫測試案例的過程來確認需求,這樣並不會花費更多時間,也可以節省誤會需求的浪費,另外,也會因為測試自動化,可以減少在測試上花的時間,也一併提昇開發的品質。

  • 自動軟體測試、TDD 與 BDD

    • TDD 則是「先寫測試再開發程式」。沒有程式要怎麼寫測試呢?除了有些工具可以讓你寫測試時,一邊幫你產生空的類別與方法外(如 Eclipse 撰寫 Java 時就有類似的功能),一般來說也可以直接假設你已經撰寫好了程式,先揣摩如果已經寫好了這個程式該要如何使用。

    • BDD 則是比起 TDD 更進一步,除了在實作前先寫測試外,而在測試前還要先寫規格,但是這份規格並不是單純的敘述軟體的功能,而是這份規格,是一份「可以執行的規格」。

  • 開發人員看測試之TDD和BDD

    • BDD是一種敏捷軟體開發的技術。它對TDD的理念進行了擴展,在TDD中側重點偏向開發,通過測試用例來規範約束開發者編寫出質量更高、bug更少的代碼。而BDD更加側重設計,其要求在設計測試用例的時候對系統進行定義,倡導使用通用的語言將系統的行為描述出來,將系統設計和測試用例結合起來,從而以此為驅動進行開發工作。

  • 淺談TDD、BDD與ATDD軟體開發

  • Software Architecture Rule of Thumb — Requirements!

    • Use Testable Acceptance Criteria

  • 關於BDD/TDD的三大誤解

    • 誤解一:TDD是一種測試方法

      • TDD是一種「開發方法」,藉由先定義規格,再撰寫程式的方式來開發軟體。

    • 誤解二:TDD很神

    • 誤解三:TDD可治百病

  • Behavior-driven development

  • Behaviour-Driven Development

  • INTRODUCING BDD (by Dan North)

    • +Title: Customer withdraws cash+

      • As a customer,

      • I want to withdraw cash from an ATM,

      • so that I don’t have to wait in line at the bank.

    • +Scenario 1: Account is in credit+

      • Given the account is in credit

        • And the card is valid

        • And the dispenser contains cash

      • When the customer requests cash

      • Then ensure the account is debited

        • And ensure cash is dispensed

        • And ensure the card is returned

    • +Scenario 2: Account is overdrawn past the overdraft limit+

      • Given the account is overdrawn

        • And the card is valid

      • When the customer requests cash

      • Then ensure a rejection message is displayed

        • And ensure cash is not dispensed

        • And ensure the card is returned

  • WHAT’S IN A STORY?

    • Story: Account Holder withdraws cash

      • As an Account Holder

      • I want to withdraw cash from an ATM

      • So that I can get money when the bank is closed

    • Scenario 1: Account has sufficient funds

      • Given the account balance is \$100

        • And the card is valid

        • And the machine contains enough money

      • When the Account Holder requests \$20

      • Then the ATM should dispense \$20

        • And the account balance should be \$80

        • And the card should be returned

    • Scenario 2: Account has insufficient funds

      • Given the account balance is \$10

        • And the card is valid

        • And the machine contains enough money

      • When the Account Holder requests \$20

      • Then the ATM should not dispense any money

        • And the ATM should say there are insufficient funds

        • And the account balance should be \$20

        • And the card should be returned

    • Scenario 3: Card has been disabled

      • Given the card is disabled

      • When the Account Holder requests \$20

      • Then the ATM should retain the card

        • And the ATM should say the card has been retained

Katalon

  • Katalon 是個非常強大的測試工具,可以進行web UI test、web API test、Mobile test。也可以透過cucumber來落實BDD。

BDD

在Katalon,測試是定義在TestCase裡,在TestCase裡可以執行Cucumber Features File,跟cucumber一樣,是利用Step Definitions去執行實際的測試 (詳參: Running Cucumber Features FileAutomation Testing with Cucumber BDD in Agile Teams)。

新增一個專案,New Sample Project,Project:Sample BDD Cucumber Tests Project

可以看到範例專案的Test Suites裡有一個有一個「Verify Operations」的suite。在這個suite裡其實是用到了 「Verify Divide」、「Verify Minus」、「Verify Multiply」、「Verify Add」等test case。在這些test case裡,使用到對應的feature,這些feature又對應了不同的step function (在include/scripts/groovy/operations裡)。

由於這些測試使用到webUI,測試的URL是: https://katalon-studio-samples.github.io/calculator/

我們先來試試看一個比較簡單的測試。

簡單的測試

先新增一個feature: Include/features/is_it_friday_yet.feature

Feature: Is it Friday yet?

Everybody wants to know when it's Friday


Scenario: Today is Tuesday

Given today is Tuesday

When I ask whether it's Friday yet

Then I should be told Nope

Scenario: Today is Friday

Given today is Friday

When I ask whether it's Friday yet

Then I should be told TGIF

新增一個step function:

scripts/groovy/com.sample.test/TestFriday.goovy

Katalon支援的是Groovy,語法跟Java類似

在step function裡,先定義given要執行的動作,內容必須跟feature內容對應,「(.*)」就是把內容變成參數,在下面的方法裡就會傳到「givenDay」裡,語法跟標準的cucumber語法有點不一樣 (目前還沒找到相關說明文件):

@Given("today is (.*)")

def Today_Is(String givenDay ) {

today = givenDay;

}

When也是同樣道理,在這裡呼叫is_it_Friday():

@When("I ask whether it's Friday yet")

def Whether_Is_It_Friday() {

actual_Answer=is_it_friday(today);

}

在Then的部分,利用assert來檢查真實的答案與預期的答案是否相同,當結果為真的時候,就是測試通過。

@Then("I should be told (.*)")

def I_verify_the_status_in_step(String expectedAnswer) {

assert actual_Answer == expectedAnswer;

}

scripts/groovy/com.sample.test/TestFriday.goovy

完整的程式:

package com.sample.test


import cucumber.api.java.en.Given

import cucumber.api.java.en.Then

import cucumber.api.java.en.When


class TestFriday {

/**

* The step definitions below match with Katalon sample Gherkin steps

*/

String today ="";

String actual_Answer="";

def is_it_friday(String today){

if (today.equals("Friday") ) {

return "TGIF";

} else {

return "Nope";

}

}

@Given("today is (.*)")

def Today_Is(String givenDay ) {

today = givenDay;

}


@When("I ask whether it's Friday yet")

def Whether_Is_It_Friday() {

actual_Answer=is_it_friday(today);

}


@Then("I should be told (.*)")

def I_verify_the_status_in_step(String expectedAnswer) {

assert actual_Answer == expectedAnswer;

}

}

執行測試,就可以看到測試的結果了。

當scenario很多的時候,可以不用寫很多個scenario,而是採用Scenario Outline,將內容參數化。

Include/features/is_it_friday_yet_again.feature

Feature: Is it Friday yet again?

Everybody wants to know when it's Friday


Scenario Outline: Today is or is not Friday

Given today is <day>

When I ask whether it's Friday yet

Then I should be told <answer>


Examples:

| day | answer |

| Friday | TGIF |

| Sunday | Nope |

| anything else! | Nope |

再進一步,假設我們要測試的是個Java類別,Friday,把以下檔案跟TestFriday.goovy放在同一個檔案夾下,這樣的測試就類似一般的單元測試:

Friday.java

package com.sample.test;


public class Friday {

String day="";

public Friday(String day){

this.day = day;

}

String isFriday(){

if (day.equals("Friday")){

return "TGIF";

}

else {

return "Nope";

}

}


}

修改一下TestFriday.goovy

scripts/groovy/com.sample.test/TestFriday.goovy

package com.sample.test


import cucumber.api.java.en.Given

import cucumber.api.java.en.Then

import cucumber.api.java.en.When


class TestFriday {

/**

* The step definitions below match with Katalon sample Gherkin steps

*/

String actual_Answer="";

Friday today;


@Given("today is (.*)")

def Today_Is(String givenDay ) {

today = new Friday(givenDay);

}


@When("I ask whether it's Friday yet")

def Whether_Is_It_Friday() {

actual_Answer=today.isFriday();

}


@Then("I should be told (.*)")

def I_verify_the_status_in_step(String expectedAnswer) {

assert actual_Answer == expectedAnswer;

}

}


目前在Katalon裡似乎指令還沒有辦法中文化,不過,內容是可以接受中文字:

Include/features/is_it_friday_yet_again.feature

@Friday

Feature: 是星期五嗎?

我們想知道今天是不是星期五


@Friday

Scenario: 今天是星期二

Given 今天是'星期二'

When 我問今天是不是星期五

Then 我應該被告知'不是星期五'

@Friday

Scenario: 今天是星期五

Given 今天是'星期五'

When 我問今天是不是星期五

Then 我應該被告知'快樂星期五'

scripts/groovy/com.sample.test/TestFriday.goovy

package com.sample.test


//import cucumber.api.java.en.And

import cucumber.api.java.en.Given

import cucumber.api.java.en.Then

import cucumber.api.java.en.When



class TestFriday {

/**

* The step definitions below match with Katalon sample Gherkin steps

*/

String actual_Answer="";

Friday today;


@Given("今天是'(.*)'")

def Today_Is(String givenDay ) {

today = new Friday(givenDay);

}


@When("我問今天是不是星期五")

def Whether_Is_It_Friday() {

actual_Answer=today.isFriday();

}


@Then("我應該被告知'(.*)'")

def I_verify_the_status_in_step(String expectedAnswer) {

assert actual_Answer == expectedAnswer;

}

}

scripts/groovy/com.sample.test/Friday.java

package com.sample.test;


public class Friday {

String day="";

public Friday(String day){

this.day = day;

}

String isFriday(){

if (day.equals("星期五")){

return "快樂星期五";

}

else {

return "不是星期五";

}

}


}

**作業**

練習使用Katalon,並且寫自己的feature及step function

***

看一下Katalon的範例,這個BDD範例結合了WebUI測試,也就是不只是可以直接測試程式碼也可以測試webUI:

@tag

Feature: The test case verifies that a user can login with a valid account


Scenario Outline: Login successfully

Given The Login page is loaded successfully

When I login the system with valid "<username>" username and "<password>" password

Then The Dashboard Page is loaded successfully


Examples:

| username | password |

| demo@katalon.com | sPiHQ&YEa6ST`de+ |

  • Include\scripts\groovy\login.LoginSteps.groovy

package login

import static com.kms.katalon.core.checkpoint.CheckpointFactory.findCheckpoint

import static com.kms.katalon.core.testcase.TestCaseFactory.findTestCase

import static com.kms.katalon.core.testdata.TestDataFactory.findTestData

import static com.kms.katalon.core.testobject.ObjectRepository.findTestObject


import com.kms.katalon.core.annotation.Keyword

import com.kms.katalon.core.checkpoint.Checkpoint

import com.kms.katalon.core.checkpoint.CheckpointFactory

import com.kms.katalon.core.mobile.keyword.MobileBuiltInKeywords

import com.kms.katalon.core.model.FailureHandling

import com.kms.katalon.core.testcase.TestCase

import com.kms.katalon.core.testcase.TestCaseFactory

import com.kms.katalon.core.testdata.TestData

import com.kms.katalon.core.testdata.TestDataFactory

import com.kms.katalon.core.testobject.ObjectRepository

import com.kms.katalon.core.testobject.TestObject

import com.kms.katalon.core.webservice.keyword.WSBuiltInKeywords

import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords


import internal.GlobalVariable


import MobileBuiltInKeywords as Mobile

import WSBuiltInKeywords as WS

import WebUiBuiltInKeywords as WebUI


import org.openqa.selenium.WebElement

import org.openqa.selenium.WebDriver

import org.openqa.selenium.By


import com.kms.katalon.core.mobile.keyword.internal.MobileDriverFactory

import com.kms.katalon.core.webui.driver.DriverFactory


import com.kms.katalon.core.testobject.RequestObject

import com.kms.katalon.core.testobject.ResponseObject

import com.kms.katalon.core.testobject.ConditionType

import com.kms.katalon.core.testobject.TestObjectProperty


import com.kms.katalon.core.mobile.helper.MobileElementCommonHelper

import com.kms.katalon.core.util.KeywordUtil


import com.kms.katalon.core.webui.exception.WebElementNotFoundException


import cucumber.api.java.en.And

import cucumber.api.java.en.Given

import cucumber.api.java.en.Then

import cucumber.api.java.en.When



class LoginSteps {


@Given('The Login page is loaded successfully')

def The_Login_page_is_loaded_successfully() {

WebUI.callTestCase(findTestCase('Web UI Tests/Advance Tests/Pages/Login Page/The Login page is loaded successfully'), [:], FailureHandling.STOP_ON_FAILURE)

}


@When('I login the system with valid "(.*)" username and "(.*)" password')

def I_login_the_system_with_valid_username_password(String username, String password) {

WebUI.callTestCase(findTestCase('Web UI Tests/Advance Tests/Pages/Login Page/Login with username and password'), [('username') : username

, ('password') : password], FailureHandling.STOP_ON_FAILURE)

}


@Then('The Dashboard Page is loaded successfully')

def I_verify_the_status_in_step() {

WebUI.callTestCase(findTestCase('Web UI Tests/Advance Tests/Pages/Dashboard Page/The Dashboard Page is loaded successfully'), [:],

FailureHandling.STOP_ON_FAILURE)

}

}

參考資料

BDD.pptx
  • SbE vs. BDD

  • Specification by Example

  • BDD

  • 使用Katalon

    • 新增一個feature

    • 新增對應的step function

    • 新增test case

Cucumber with Javascript / react

npm install cucumber

features\is_it_Friday_yet.feature

Feature: Is it Friday yet?

Everybody wants to know when it's Friday


Scenario Outline: Today is or is not Friday

Given today is "<day>"

When I ask whether it's Friday yet

Then I should be told "<answer>"


Examples:

| day | answer |

| Friday | TGIF |

| Sunday | Nope |

| anything else! | Nope |

中文版

# language: zh-TW

功能: 是星期五嗎?

我們想知道今天是不是星期五


場景大綱: 今天是或不是星期五

假如 今天是 "<day>"

當 我問今天是不是星期五

那麼 我應該被告知 "<answer>"


例子:

| day | answer |

| 5 | 快樂星期五 |

| 0 | 不是星期五 |

| 4 | 不是星期五 |

| 1 | 不是星期五 |

利用step definition來測試程式碼。如: features\step_definitions\stepdef.js

const assert = require('assert');

const { Given, When, Then } = require('cucumber');


function isItFriday(today) {

if (today === "5") {

return "快樂星期五";

} else {

return "不是星期五";

}

}

Given('今天是 {string}', function (givenDay) {

this.today = givenDay;

});

When('我問今天是不是星期五', function () {

this.actualAnswer = isItFriday(this.today);

});

Then('我應該被告知 {string}', function (expectedAnswer) {

assert.equal(this.actualAnswer, expectedAnswer);

});

如果還沒安裝過typescript,也可以不使用typescript

npm install typescript

利用create-react-app產生範例並開啟typescript

npx create-react-app . --typescript

npm install jest-cucumber

src/features/is_it_friday_yet.feature

Feature: Is it Friday yet?

Everybody wants to know when it's Friday


Scenario Outline: Today is or is not Friday

Given today is "<day>"

When I ask whether it's Friday yet

Then I should be told "<answer>"


Examples:

| day | answer |

| Friday | TGIF |

| Sunday | Nope |

| anything else! | Nope |

src/features/step_definitions/is_it_friday_yet.steps.test.js

//jest-cucumber in javascript

import { defineFeature, loadFeature } from 'jest-cucumber';

import isItFriday from '../../utils/friday';


const feature = loadFeature('./src/features/is_it_friday_yet.feature');


defineFeature(feature, test => {

test('Today is or is not Friday', ({ given, when, then }) => {

let today;

let actualAnswer;

//regular expressions for parameter

given(/^today is (.*)$/, givenDay=>{

today = givenDay;

});

when('I ask whether it\'s Friday yet', () => {

actualAnswer = isItFriday(today);

});

//regular expressions for parameter

then(/^I should be told (.*)$/, expectedAnswer => {

expect(actualAnswer===expectedAnswer);

});

});

});

也可以寫jest的測試,如:src/utils/friday.test.js,也有人認為這樣的寫法(describe/test/expect)就是BDD。不過,應該是比較像cucumber的step definition,適合程式設計師閱讀,一般使用者應該不太容易看得懂這樣的內容。

import isItFriday from './friday';


describe('Today is or is not Friday',() => {

test('Sunday is "Nope"', () => {

expect(isItFriday('Sunday')==='Nope');

});

test('Friday is "TGIF"', () => {

expect(isItFriday('Friday')==='TGIF');

});

});


describe.each`

day | answer

${'Friday'} | ${'TGIF'}

${'Sunday'} | ${'Nope'}

${'anything else'} | ${'Nope'}

`('$day + $answer', ({day,answer}) => {

test(`Sunday is ${answer}`, () => {

expect(isItFriday(day)===answer);

});

});

src/utils/friday.js

export default (today) => {

if (today === "Friday") {

return "TGIF";

} else {

return "Nope";

}};