John Ferguson Smart
Behavior-Driven Development on the JVMA State of the Union
John Ferguson Smart
ConsultantTrainerMentorAuthorSpeakerCoder
What is BDD?
Common Language
Business Developer
Business Analyst
Tester
Executable Specifications
OutsideIn
Value Driven
\So why use BDD?
Only build features that add real value
Less wasted effort
Better communication
Higher quality, better tested product
Traceability
BDD - Requirements Analysis and Communication
Examples/Scenarios
Stories
Features
Capabilities
Goals
Acceptance Criteria
11
“We are going to build an online classifieds website”
Successful projects start with a shared vision
12
You define goals to achieve your vision
“We can increase advertising revenue by letting sellers post their classified ads online”
“Let’s get more sales for our advertisers by making the ads easier to find online.”
A good goal should add value to the businessIncrease revenueReduce costsAvoid future costsProtect revenue
Determining the value of a goal
“Increase advertising revenue by allowing sellers to post classified ads online”
“Reduce the costs involved in publishing a classified ad by allowing sellers to post them online themselves. ”
“Prevent current customers switching to a competing product by providing support for online credit card payments”
What does the customer really need?
I want users to be able to search for products by keyword Why?
So that potential buyers can find the articles they want
So that our sellers can sell their stuff fasterWhy?
Why?
So that they keep selling their stuff on our siteWhy?
So that we keep earning money when they post their ads with us
What does the customer really need?
Good teams push back!Users tend to express requirements as implementationsWe need to find the business need behind the suggested implementation
I want users to be able to search by keyword
So in order to make the site more attractive for sellersBuyers need to be able to find things easily
A search feature might be one way to achieve this
But full-text searches might be more effective than keywords
“Let’s get more sales for our advertisers by making the ads easier to find online.”
Notify potential buyers about new itemsIn order to increase sales of advertised articlesAs a sellerI want previous buyers to know about new items that they might be interested in buying
Search for online adsIn order to increase sales of advertised articlesAs a sellerI want buyers to be able to easily find ads for articles they want to buy
Features and capabilities help deliver these goals
Feature Injection - what features do you do first?
Our goals say what business value we need to deliverWe implement the minimum features required to deliver this business value
Search for online adsIn order to increase sales of advertised articlesAs a sellerI want buyers to be able to easily find ads for articles they want to buy
The goal comes first
The stakeholder is secondary
The feature must be required to achieve the goal
18
We use examples and stories to explore the features
“Searching by category”
Search for online ads
“Searching by keyword and category”
19
We use examples and stories to explore the features
Search for online ads
Searching by keyword and locationGiven Sally wants to buy a puppy for her son
When she looks for ‘puppy’ in the ‘Pets and Animals’ category
Then she should obtain a list of ads for puppies for sale.
20
Examples and scenarios become acceptance criteria
Acceptance Criteria illustrate and validate the stories
Searching by keyword and locationGiven Sally wants to buy a puppy for her son
When she looks for ‘puppy’ in the ‘Pets and Animals’ category
Then she should obtain a list of ads for puppies for sale.
Scenario: Searching by keyword and location
Given Sally wants to buy a present for her son When she looks for the present in a given category Then she should obtain a list of matching ads for sale.
Examples:
Present Category Expected Keywordspuppy Pets & Animals labradorkitten Pets & Animals burmesekitten Toys fluffy cat
Organize your requirements
Requirements can come from many sources...
CapabilityIn order to increase the number of items I sellAs a sellerI want buyers to be able to view ads for items they might want to purchase
FeatureIn order to increase sales of advertised articlesAs a sellerI want potential buyers to be able to display only the ads for articles that they might be interested in purchasing.
StoryIn order to find the items I am interested in fasterAs a buyerI want to be able to list all the ads with a particular keyword in the description or title.
Goal: In order to increase revenue from commissions on classified ads salesAs the head of the classified ads departmentI want to increase the number of items sold via our classified ads
Keep them organized!
Requirements can come from many sources...
25
BDD - Test Automation and Beyond
26
The original Java BDD framework
27
Narrative:In order to increase sales of advertised articlesAs a sellerI want buyers to be able to easily find ads for articles they want to buy
Scenario: Searching by keyword and location
Given Sally wants to buy a puppy for her son When she looks for 'puppy' in the 'Pets and Animals' category Then she should obtain a list of ads for puppies for sale.
search_by_keyword_and_location.story
28
Scenario: Searching by keyword and location
Given Sally wants to buy a puppy for her son When she looks for 'puppy' in the 'Pets and Animals' category Then she should obtain a list of ads for puppies for sale.
search_by_keyword_and_location.story
Scenario: Searching by keyword and location
Given Sally wants to buy a <present> for her son When she looks for '<present>' in the '<category>' category Then she should obtain a list of ads for <expected> for sale.
Examples:|present |category |expected||puppy |Pets & Animals | puppies||kitten |Pets & Animals | kittens||seiko |Jewellery & Watches| watch |
29
Scenario: Searching by keyword and location
Given Sally wants to buy a puppy for her son When she looks for 'puppy' in the 'Pets and Animals' category Then she should obtain a list of ads for puppies for sale.
search_by_keyword_and_location.story
1
30
Scenario: Searching by keyword and location
Given Sally wants to buy a puppy for her son When she looks for 'puppy' in the 'Pets and Animals' category Then she should obtain a list of puppy ads
search_by_keyword_and_location.story
1
public class SearchAdsSteps { @Steps BuyerSteps buyer;
@Given("Sally wants to buy a $present for her son") public void buyingAPresent(String present) { buyer.opens_home_page(); }
@When("she looks for $keyword in the $category category") public void adSearchByCategoryAndKeyword(String category, String keyword) { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); }
@Then("she should obtain a list of $keyword ads") public void shouldOnlySeeAdsContainingKeyword(String keyword) { buyer.should_only_see_results_with_titles_containing(keyword); }}
2
31
Scenario: Searching by keyword and location
Given Sally wants to buy a puppy for her son When she looks for 'puppy' in the 'Pets and Animals' category Then she should obtain a list of puppy ads
search_by_keyword_and_location.story
1
public class SearchAdsSteps { @Steps BuyerSteps buyer;
@Given("Sally wants to buy a $present for her son") public void buyingAPresent(String present) { buyer.opens_home_page(); }
@When("she looks for $keyword in the $category category") public void adSearchByCategoryAndKeyword(String category, String keyword) { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); }
@Then("she should obtain a list of $keyword ads") public void shouldOnlySeeAdsContainingKeyword(String keyword) { buyer.should_only_see_results_with_titles_containing(keyword); }}
2public class BuyerStories extends JUnitStories { public BuyerStories() { configuredEmbedder().embedderControls().doGenerateViewAfterStories(true).doIgnoreFailureInStories(false) .doIgnoreFailureInView(true).doVerboseFailures(true).useThreads(2).useStoryTimeoutInSecs(60); }
@Override public Configuration configuration() { return new MostUsefulConfiguration(); }
@Override public InjectableStepsFactory stepsFactory() { return new InstanceStepsFactory(configuration(), new TraderSteps(new TradingService()), new AndSteps()); }
@Override protected List<String> storyPaths() { String codeLocation = codeLocationFromClass(this.getClass()).getFile(); return new StoryFinder().findPaths(codeLocation, asList("**/*.story", "**/traders_can_be_subset.story"), asList(""), "file:" + codeLocation); }}
3
32
Scenario: Searching by keyword and location
Given Sally wants to buy a puppy for her son When she looks for 'puppy' in the 'Pets and Animals' category Then she should obtain a list of puppy ads
search_by_keyword_and_location.story
1
public class SearchAdsSteps { @Steps BuyerSteps buyer;
@Given("Sally wants to buy a $present for her son") public void buyingAPresent(String present) { buyer.opens_home_page(); }
@When("she looks for $keyword in the $category category") public void adSearchByCategoryAndKeyword(String category, String keyword) { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); }
@Then("she should obtain a list of $keyword ads") public void shouldOnlySeeAdsContainingKeyword(String keyword) { buyer.should_only_see_results_with_titles_containing(keyword); }}
public class BuyerStories extends ThucydidesJUnitStories {}
3’
2
33
Now available in JVM flavor!
34
Feature:In order to increase sales of advertised articlesAs a sellerI want buyers to be able to easily find ads for articles they want to buy
Scenario: Searching by keyword and location
Given Sally wants to buy a "puppy" for her son When she looks for "puppy" in the "Pets and Animals" category Then she should obtain a list of "puppy" ads
1
35
Scenario: Searching by keyword and location
Given Sally wants to buy a "puppy" for her son When she looks for "puppy" in the "Pets and Animals" category Then she should obtain a list of "puppy" ads
Scenario: Searching by keyword and location
Given Sally wants to buy a <present> for her son When she looks for '<present>' in the '<category>' category Then she should obtain a list of ads for <expected> for sale.
Examples:|present |category |expected||puppy |Pets & Animals | puppies||kitten |Pets & Animals | kittens||seiko |Jewellery & Watches| watch |
36
Scenario: Searching by keyword and location
Given Sally wants to buy a "puppy" for her son When she looks for "puppy" in the "Pets and Animals" category Then she should obtain a list of "puppy" ads
import org.junit.runner.RunWith;import cucumber.junit.Cucumber;
@RunWith(Cucumber.class)@Cucumber.Options(format={"pretty", "html:target/cucumber"})public class RunTests {}
1
2
37
Scenario: Searching by keyword and location
Given Sally wants to buy a "puppy" for her son When she looks for "puppy" in the "Pets and Animals" category Then she should obtain a list of "puppy" ads
import org.junit.runner.RunWith;import cucumber.junit.Cucumber;
@RunWith(Cucumber.class)@Cucumber.Options(format={"pretty", "html:target/cucumber"})public class RunTests {}
1
2public class SearchAdsSteps { @Steps BuyerSteps buyer;
@Given("^Sally wants to buy a \"([^\"]*)\" for her son$") public void buyingAPresent(String present) { buyer.opens_home_page(); }
@When("^she looks for \"([^\"]*)\" in the \"([^\"]*)\" category$") public void adSearchByCategoryAndKeyword(String category, String keyword) { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); }
@Then("^she should obtain a list of \"([^\"]*)\" ads$") public void shouldOnlySeeAdsContainingKeyword(String keyword) { buyer.should_only_see_results_with_titles_containing(keyword); }}
3
38
100% Groovy
39
scenario "Searching by keyword and location", { given "Sally wants to buy a puppy for her son" when "she looks for 'puppy' in the 'Pets and Animals' category" then "she should obtain a list of ads for puppies for sale"}
search_by_keyword_and_location.story
scenario "Searching by keyword and location", { given "Sally wants to buy a #present for her son" when "she looks for '#present' in the '#category' category" then "she should obtain a list of ads for #expected for sale" where "examples should be", { present = ['puppy', 'kitten', 'seiko'] category = ['Pets & Animals','Pets & Animals', 'Jewellery & Watches'] expected = ['puppies', 'kittens', 'watch'] }}
40
scenario "Searching by keyword and location", { given "Sally wants to buy a puppy for her son" when "she looks for 'puppy' in the 'Pets and Animals' category" then "she should obtain a list of ads for puppies for sale"}
search_by_keyword_and_location.story
1
41
scenario "Searching by keyword and location", { given "Sally wants to buy a puppy for her son" when "she looks for 'puppy' in the 'Pets and Animals' category" then "she should obtain a list of ads for puppies for sale"}
search_by_keyword_and_location.story
1
using "thucydides"
thucydides.uses_steps_from BuyerSteps
scenario "Searching by keyword and location", { given "Sally wants to buy a puppy for her son", { buyer.opens_home_page() } when "she looks for 'puppy' in the 'Pets and Animals' category", { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); } then "she should obtain a list of ads for puppies for sale",{ buyer.should_only_see_results_with_titles_containing keyword }}
2
42
Keeping an eye on things
43
(Think “Two-CDs”)
44
Scenario: Searching by keywordGiven Sally wants to buy a puppy for her sonWhen she looks for ads in the Pets & Animals category containing puppyThen she should obtain a list of ads for puppies for sale
45
Scenario: Searching by keywordGiven Sally wants to buy a puppy for her sonWhen she looks for ads in the Pets & Animals category containing puppyThen she should obtain a list of ads for puppies for salepublic class SearchAdsSteps {
@Steps BuyerSteps buyer;
@Given("Sally wants to buy a $present for her son") public void buyingAPresent(String present) { buyer.opens_home_page(); }
@When("she looks for $keyword in the $category category") public void adSearchByCategoryAndKeyword(String category, String keyword) { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); }
@Then("she should obtain a list of $keyword ads") public void shouldOnlySeeAdsContainingKeyword(String keyword) { buyer.should_only_see_results_with_titles_containing(keyword); }}
46
public class SearchAdsSteps { @Steps BuyerSteps buyer;
@Given("Sally wants to buy a $present for her son") public void buyingAPresent(String present) { buyer.opens_home_page(); }
@When("she looks for $keyword in the $category category") public void adSearchByCategoryAndKeyword(String category, String keyword) { buyer.chooses_category_and_keywords(category, keyword); buyer.performs_search(); }
@Then("she should obtain a list of $keyword ads") public void shouldOnlySeeAdsContainingKeyword(String keyword) { buyer.should_only_see_results_with_titles_containing(keyword); }}
public class BuyerSteps extends ScenarioSteps {
HomePage homePage; SearchResultsPage searchResultsPage;
public BuyerSteps(Pages pages) { super(pages); homePage = getPages().get(HomePage.class); searchResultsPage = getPages().get(SearchResultsPage.class); }
@Step public void opens_home_page() { homePage.open(); }
@Step public void chooses_region(String region) { homePage.chooseRegion(region); }
@Step public void chooses_category_and_keywords(String category, String keywords) { homePage.chooseCategoryFromDropdown(category); homePage.enterKeywords(keywords); }
47
48
49
50
51
52
From Acceptance Tests to Developer Tests
BDD - A Development Tool
TDD or BDD?
Write a failing test
Make it pass
Refactor
TDD
What test should I write?
Developer Tests (low level features)
Acceptance Tests (high level features)
etc.
Spock
What features should I implement?
Story: In order to find the items I am interested in fasterAs a buyerI want to be able to list all the ads with a particular keyword in the description or title.
Goal: In order to increase revenue from commissions on classified ads salesAs the head of the classified ads departmentI want to increase the number of items sold via our classified ads
Scenario: Searching by keyword and locationGiven Sally wants to buy a puppy for her sonWhen she looks for ads in the Pets & Animals category containing puppy in New South Wales
Scenario: Searching by keyword and locationGiven Sally wants to buy a puppy for her sonWhen she looks for ads in the Pets & Animals category containing puppy in New South Wales
Scenario: Searching by keywordGiven Sally wants to buy a puppy for her sonWhen she looks for ads in the Pets & Animals category containing puppyThen she should obtain a list of ads for puppies for sale
Acceptance Tests
Developer Tests
class WhenCalculatingGST extends Specification {
def "GST should apply on ordinary articles"() { given: "we are selling a shirt" def sale = Sale.of(1,"shirt").forANetPriceOf(10.00) when: "we calculate the price including GST" def totalPrice = sale.totalPrice then: "the price should include GST of 10%" totalPrice == 11.00 }}
class WhenCalculatingGST extends Specification {
def "GST should apply on ordinary articles"() { given: "we are selling a shirt" def sale = Sale.of(1,"shirt").forANetPriceOf(10.00) when: "we calculate the price including GST" def totalPrice = sale.totalPrice then: "the price should include GST of 10%" totalPrice == 11.00 }}
class WhenCalculatingGST extends Specification {
def "GST should apply on ordinary articles"() { given: "we are selling a shirt" def sale = Sale.of(1,"shirt").forANetPriceOf(10.00) when: "we calculate the price including GST" def totalPrice = sale.totalPrice then: "the price should include GST of 10%" totalPrice == 11.00 }}
Unit Tests Acceptance tests
Spock - BDD for developers
Spockclass WhenCalculatingGST extends Specification {
def "GST should apply on ordinary articles"() { given: "we are selling a shirt" def sale = Sale.of(1,"shirt").forANetPriceOf(10.00) when: "we calculate the price including GST" def totalPrice = sale.totalPrice then: "the price should include GST of 10%" totalPrice == 11.00 }}
Given-When-Then structure
Spockclass WhenCalculatingGST extends Specification {
...
def "GST should not apply on GST-exempt articles"() { given: "we are selling a bottle of milk" def sale = Sale.of(1,"shirt").forANetPriceOf(5.00) when: "we calculate the price including GST" def totalPrice = sale.totalPrice then: "the price should not include GST%" totalPrice == 5.00 }}
Meaningful error messages
Spock
class WhenCalculatingGST extends Specification {
def "GST should apply on ordinary articles"() { given: "GST is at 12.5%" def gstRateProvider = Mock(GSTRateProvider) gstRateProvider.getRate() >> 0.125 Sales sales = new Sales(gstRateProvider) and: "we are selling a shirt" def sale = sales.makeSaleOf(1,"shirt").forANetPriceOf(10.00) when: "we calculate the price including GST" def totalPrice = sale.totalPrice then: "the price should include GST of 12.5%" totalPrice == 11.25 }}
Lightweight stubbing
Spock
class WhenDeliveringSoldItems extends Specification {
def gstRateProvider = Mock(GSTRateProvider) def deliveryService = Mock(DeliveryService)
def "Sold articles should be delivered"() { given: "we are selling shirts online" Sales sales = new Sales(gstRateProvider, deliveryService) when: "we sell a shirt" sales.makeSaleOf(1,"shirt").forANetPriceOf(10.00) then: "the shirt should be sent to the delivery service" 1 * deliveryService.dispatch(_) }}
Lightweight mocking
class WhenDisplayingTagNamesInAReadableForm extends Specification {
def inflection = Inflector.instance
def "should transform singular nouns into plurals"() {
when: "I find the plural form of a single word" def pluralForm = inflection.of(singleForm).inPluralForm().toString(); then: "the plural form should be gramatically correct" pluralForm == expectedPluralForm where: singleForm | expectedPluralForm 'epic' | 'epics' 'feature' | 'features' 'story' | 'stories' 'stories' | 'stories' 'octopus' | 'octopi' 'sheep' | 'sheep' }}
Spock
Data-driven tests
Spec2 - BDD for Scala
class WhenCalculatingGST extends Specification { sequential
"GST should apply on ordinary articles" >> { "Given we are selling a shirt" >> { sale = Sale.of(1, "shirt").forANetPriceOf(10.00) } "When we calculate the price including GST" >> { totalPrice = sale.totalPrice } "Then the price should include a GST of 10%" >> { totalPrice === 11.00 } }
var sale = Sale(); var totalPrice = 0.0}
class WhenCalculatingGST2 extends Specification with Mockito { sequential
"GST should apply on ordinary articles" >> { "Given we are selling a shirt" >> { val sales = Sales(mock[GSTProvider]) sales.gstProvider.rate returns 12.5
sale = sales.makeSaleOf(1, "shirt").forANetPriceOf(10.00) } "When we calculate the price including GST" >> { totalPrice = sale.totalPrice } "Then the price should include a GST of 12.5%" >> { totalPrice === 11.25 } }
var sale = Sale(); var totalPrice = 0.0}
Lightweight stubbing DSL
class WhenDeliveringSoldItems extends Specification with Mockito { sequential
"Sold articles should be delivered" >> { "Given we are selling shirts online" >> { sales = Sales(mock[GSTProvider], mock[DeliveryService]) } "When we sell a shirt" >> { sale = sales.makeSaleOf(1, "shirt").forANetPriceOf(10.00) } "Then the shirt should be sent to the delivery service" >> { there was one(sales.deliveryService).dispatch(anyString) } }
var sale = Sale(); var sales = Sales()}
Lightweight mocking DSL
class WhenDisplayingTagNamesInAReadableForm extends Specification with Tables {
"The inflector should transform singular nouns into plurals" >> { """ when I find the plural form of a single word, then the plural form should be gramatically correct: """ >> { "single form" | "plural form" |> "epic" ! "epics" | "feature" ! "features" | "story" ! "story" | "stories" ! "stories" | "octopus" ! "octopi" | "sheep" ! "sheep" | { (singleForm, pluralForm) =>
Inflection.of(singleForm).inPluralForm.toString === pluralForm
} } }}
Data-driven tests, Scala-style
Jasmine - BDD for Javascript
describe( "temperature converter", function () { it("converts fahrenheit to celsius", function () { expect(Convert(50, "F").to("C")).toEqual(10); }); });
Simple assertion structure
describe( "temperature converter", function () { it("converts fahrenheit to celsius", function () { expect(Convert(50, "F").to("C")).toEqual(10); }); it("converts celsius to fahrenheit", function () { expect(Convert(30, "C").to("F")).toEqual(86); }); });
More complex behavior
describe( "converter library", function () { describe( "temperature converter", function () { it("converts fahrenheit to celsius", function () { expect(Convert(50, "F").to("C")).toEqual(10); }); it("converts celsius to fahrenheit", function () { expect(Convert(30, "C").to("F")).toEqual(86); }); });
describe( "weight converter", function () { it("converts kilograms to pounds", function () { expect(Convert(100, "KG").to("LB")).toEqual(220); }); });});
Nested behaviors
And it works with Maven!
Evaluate test results in a browser
Evaluate test results in a browser
Generate JUnit-compatible results
It’s behavior all the way down
In conclusion...
Thank You
John Ferguson Smart
Top Related