object-oriented bdd w/ cucumber by matt van horn
DESCRIPTION
Here are the slides that Matt van Horn from New Relic presented at last night's Automated Testing San Francisco meetup, hosted by Constant Contact. This presentation briefly covers continuous integration at New Relic, and then dives deeper into Object-Oriented BDD with Cucumber. We thank Matt for the great presentation. Please feel free to connect with Matt on Github or Twitter: github.com/mattvanhorn or @nycplayer http://www.meetup.com/Automated-Testing-San-Francisco/TRANSCRIPT
OBJECT ORIENTED BDD
with CUCUMBER
Thursday, October 3, 13
Who Am I?
Senior So(ware Engineer at New Relic
20 years of web applica9on development
12 years of TDD
5 years of BDD
github.com/maFvanhorn or @nycplayer
Thursday, October 3, 13
New Relic CI
Thursday, October 3, 13
New Relic CI
Github, TDDium, Jenkins
Feature branches, Pull Requests
Automa9c builds, Code reviews
Automated deploys, but some manual control
Thursday, October 3, 13
New Relic CI
We deploy 3x per day
Automated tes9ng is part of the process
However...
Thursday, October 3, 13
New Relic CI
We have a large, slow test suite
We have a large, legacy Rails app
We have a lack of up-‐to-‐date documenta9on
We o(en have communica9on issues
Thursday, October 3, 13
New Relic CI
We want to go from 3x/day to ‘at will’
We are dedica9ng 9me to improving our tests
Automated acceptance tests are key
(Faster unit tests are also important)
Thursday, October 3, 13
BehaviorDrivendevelopment
Thursday, October 3, 13
BehaviorDrivendevelopmentBDD is a second-‐genera9on, outside–in, pull-‐based, mul9ple-‐stakeholder, mul9ple-‐scale, high-‐automa9on, agile methodology. It describes a cycle of interac9ons with well-‐defined outputs, resul9ng in the delivery of working, tested so(ware that maFers.
-‐ Dan North
Thursday, October 3, 13
BehaviorDrivenDevelopment
“BDD describes TDD done well”
-‐ MaF Wynne
Thursday, October 3, 13
BehaviorDrivenDevelopment“Can you give me some examples (of using it)?”
vs.
“What are the requirements (for implemen9ng it)?”
Thursday, October 3, 13
Outside In
Start with a conversa9on
Determine the business value
Provide examples of use
Thursday, October 3, 13
Outside In User Interface
Browser
Views
Controllers
Models
Thursday, October 3, 13
Outside In User Interface
Browser
Views
Controllers
Models
Cucumber
RSpec
Thursday, October 3, 13
Outside InRed, Green, REFACTOR
Outer loop: Features, stories, scenarios
Inner loop: Unit tests, classes, methods
Cucumber
Thursday, October 3, 13
Red, Green, REFACTOR
Outer loop: Features, stories, scenarios
Inner loop: Unit tests, classes, methods
Outside In Cucumber
RSpec
Thursday, October 3, 13
Outside In Cucumber
RSpecRed, Green, REFACTOR
Outer loop: Features, stories, scenarios
Inner loop: Unit tests, classes, methods
Thursday, October 3, 13
Outside In Cucumber
RSpecRed, Green, REFACTOR
Outer loop: Features, stories, scenarios
Inner loop: Unit tests, classes, methods
Thursday, October 3, 13
BDD TOOLS
Cucumber, RSpec, FitNesse
Gherkin
Spinach, Turnip, Steak, Filet
Capybara, Wa9r, WebRAT
Selenium, Webkit, PhantomJS, Rack::Test
Thursday, October 3, 13
Cucumber Complaints
Don’t need English, code is fine
Doesn’t save developer 9me
BriFle tests, needing constant maintenance
Thursday, October 3, 13
You’reDoING ITWRONG
Thursday, October 3, 13
Cucumber Done right
Minimizes miscommunica9on
Hides implementa9on details
Provides robust regression tests
Communicates inten9ons
Thursday, October 3, 13
Cucumber Done right
Cucumber allows us to inform, in plain English, the intended behavior of applica9ons we build to future developers, rather than forcing them to spelunk through code to figure it out.
-‐ Ma, Polito
Thursday, October 3, 13
Object ORientedCucumber
Thursday, October 3, 13
Object ORientedCucumber
Keep data and behavior together
Hide implementa9on details
Send messages to accomplish tasks
Thursday, October 3, 13
Page Object
URLs
UI Elements
Page Sections
DomainAction
Methods
BrowserDriver
Thursday, October 3, 13
Page Object
Provides a model for a web UI
Hides details of dealing with browser
Keeps UI details in one place
Enables expressive test code
Thursday, October 3, 13
Site Prism
Open source Ruby gem
Provides simple DSL for page Objects
Exposes Capybara nodes
Thursday, October 3, 13
Technique
Plain English, not code (or pseudo-‐code)
Domain concepts, not implementa9on details
Focus on the value, not the incidentals
Thursday, October 3, 13
Stories & Scenarios Impera9ve
Scenario: Typical Meetup
Given I am on the estimate page When I fill in "Guest count" with "10" And I fill in "Slice count" with "2" And I press "Get Estimate" Then I should see "You will need to order 3 pizzas"
Thursday, October 3, 13
“English -‐ we hateses it”
Scenario: Typical Meetup
Given I am on “/estimates/new” And I fill in "input#guests" with "10" And I fill in "input#slices" with "2" And I press "input[type=’submit’]" Then I should see "You will need to order 3 pizzas"
Stories & Scenarios
Thursday, October 3, 13
Stories & Scenarios
Declara9ve
Scenario: Typical Meetup (Guests eat 2 slices each)
Given There are 10 guests expected When I ask how much to order Then I will know I need to buy 3 pizzas
Thursday, October 3, 13
ExamplesFeature: Estimating Pizza Requirements In order to avoid wasting either pizza or money As an organizer I want to know how many pizzas I need to order
Background: Given there are 10 guests expected
Scenario: Typical meetup (Guests eat 2 slices) Given the guests are hungry When I ask how much to order Then I will know I need to buy 3 pizzas
Scenario: Late-night meetup (Guests eat 3 slices) Given the guests are starving When I ask how much to order Then I will know I need to buy 4 pizzas
Scenario: After-lunch meetup (Guests eat 1 slice) Given the guests are full When I ask how much to order Then I will know I need to buy 2 pizzas
Thursday, October 3, 13
Step Definitions
Use methods that relate to the domain
Avoid nes9ng steps
Mostly black box
Some white box can be very useful
Thursday, October 3, 13
ExamplesGiven(/^there are (\d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_countend
Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_levelend
When 'I ask how much to order' do Site.new_estimate_page.request_estimateend
Then(/^I will know I need to buy (\d+ pizzas)$/) do |pie_count| expect(Site.new_estimate_page).to have_text("#{pie_count}")end
Thursday, October 3, 13
ExamplesGiven(/^there are (\d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_countend
Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_levelend
When 'I ask how much to order' do Site.new_estimate_page.request_estimateend
Then(/^I will know I need to buy (\d+ pizzas)$/) do |pie_count| expect(Site.new_estimate_page).to have_text("#{pie_count}")end
Thursday, October 3, 13
Examples# Utility class to provide easy access to page objects.class Site def self.current_page @current_page end
def self.method_missing(meth_name, *args) klass = meth_name.to_s.classify @current_page = klass.constantize.new endend
Thursday, October 3, 13
Examplesclass NewEstimatePage < SitePrism::Page URL = Rails.application.routes.url_helpers.root_path
set_url URL set_url_matcher %r(#{URL})
element :guests_field, "input[name='estimate[guest_count]']" element :slices_field, "input[name='estimate[slice_count]']" element :submit_btn, "input[name='commit']"
# ... instance methods elided ...
end
Thursday, October 3, 13
Examplesclass NewEstimatePage < SitePrism::Page URL = Rails.application.routes.url_helpers.root_path
set_url URL set_url_matcher %r(#{URL})
element :guests_field, "input[name='estimate[guest_count]']" element :slices_field, "input[name='estimate[slice_count]']" element :submit_btn, "input[name='commit']"
# ... instance methods elided ...
end
Thursday, October 3, 13
Examplesclass NewEstimatePage < SitePrism::Page URL = Rails.application.routes.url_helpers.root_path
set_url URL set_url_matcher %r(#{URL})
element :guests_field, "input[name='estimate[guest_count]']" element :slices_field, "input[name='estimate[slice_count]']" element :submit_btn, "input[name='commit']"
# ... instance methods elided ...
end
Thursday, October 3, 13
Examplesclass NewEstimatePage < SitePrism::Page # ... DSL elided ...
def guests_expected=(guest_count) load unless displayed? guests_field.set guest_count end
def hunger_level=(hunger) load unless displayed? slices_per_person = case hunger when 'full' then 1 when 'hungry' then 2 when 'starving' then 3 end slices_field.set slices_per_person end
def request_estimate load unless displayed? submit_btn.click endend
Thursday, October 3, 13
DEMO
Thursday, October 3, 13
DEMO
Thursday, October 3, 13
Changes
Thursday, October 3, 13
Product Manager:
“We should es9mate beer as well”
“It should work the same way as pizza”
Thursday, October 3, 13
Application Changes:
Change es9mate model
Change form view
Change es9mate view
Change es9mate view helper
Thursday, October 3, 13
Cucumber Changes:
Change scenarios
Add steps for new scenario changes
Add elements to page model
Add methods to page model to use elements
Thursday, October 3, 13
Examples Scenario: Typical meetup Given the guests are hungry And the guests love beer When I ask how much to order Then I will know I need to buy 3 pizza pies And I will know I need to buy 1 case and 1 six-pack of beer
Scenario: Late-night meetup Given the guests are starving And the guests like beer When I ask how much to order Then I will know I need to buy 4 pizza pies And I will know I need to buy 1 case of beer
Scenario: After-lunch meetup Given the guests are full And the guests are underage When I ask how much to order Then I will know I need to buy 2 pizza pies And I will know I don't need to buy beer
Thursday, October 3, 13
ExamplesGiven(/^the guests (like|love) beer$/) do |thirst| Site.new_estimate_page.thirst_level = thirstend
Given(/^the guests are underage$/) do Site.new_estimate_page.thirst_level = 'none'end
Then(/^I will know I need to buy ((?:\d+ cases?)?(?: and )?(?:\d+ six\-packs?)? of beer)$/) do |content| expect(Site.new_estimate_page).to have_text(content)end
Then(/^I will know I don't need to buy beer$/) do expect(Site.new_estimate_page).to have_text("no beer")end
Thursday, October 3, 13
Examplesclass NewEstimatePage < SitePrism::Page
# ... URL matchers elided ...
element :guests_field, "input[name='estimate[guest_count]']" element :slices_field, "input[name='estimate[slice_count]']" element :beers_field, "input[name='estimate[beer_count]']" element :submit_btn, "input[name='commit']"
# ... other methods elided ...
def thirst_level=(thirst) load unless displayed? beer_count = case thirst when 'none' then 0 when 'like' then 2 when 'love' then 3 end beers_field.set beer_count endend
Thursday, October 3, 13
Examples
Thursday, October 3, 13
Examples
Thursday, October 3, 13
Refactor
Thursday, October 3, 13
ExamplesGiven(/^there are (\d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_countend
Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_levelend
When 'I ask how much to order' do Site.new_estimate_page.request_estimateend
Then(/^I will know I need to buy (\d+ pizza pies)$/) do |pie_count| expect(Site.new_estimate_page).to have_text("#{pie_count}")end
Thursday, October 3, 13
HELPER ModulesHelpers are glue between inten9on and implementa9on
Swapping out helpers can adapt your suite to different plajorms or devices
Page Objects are for page based UIs, but the principles can be applied to other domains
Thursday, October 3, 13
Examplesmodule WebHelper def guests_expected count Site.new_estimate_page.guests_expected = count end
def general_hunger_level hunger Site.new_estimate_page.hunger_level = hunger end
def general_thirst_level thirst Site.new_estimate_page.thirst_level = thirst end
def submit_request_for_estimate Site.new_estimate_page.request_estimate end
def verify_pizzas_needed num_pies expect(Site.new_estimate_page).to have_text("#{num_pies} pizza pies") end
def verify_beer_needed beer_text expect(Site.new_estimate_page).to have_text(beer_text) endendWorld(WebHelper)
Thursday, October 3, 13
ExamplesTransform /(\d+)/ do |num| num.to_iend
Given /^there are (\d+) guests expected$/ do |guest_count| guests_expected guest_countend
Given /^the guests are (full|hungry|starving)$/ do |hunger| general_hunger_level hungerend
Given /^the guests (like|love) beer$/ do |thirst| general_thirst_level thirstend
When 'I ask how much to order' do submit_request_for_estimateend
Then(/^I will know I need to buy (\d+) pizza pies$/) do |pie_count| verify_pizzas_needed pie_countend
Then(/^I will know I need to buy ((?:\d+ cases?)?(?: and )?(?:\d+ six\-packs?)? of beer)$/) do |beer_text| verify_beer_needed beer_textend
Thursday, October 3, 13
Product Manager:
“It’s too easy to make typos.”
“Last week Ted ordered 33 beers per person.”
“The carpet is s9ll not completely clean.”
Thursday, October 3, 13
Application Changes:
Change form view to use different widgets
Thursday, October 3, 13
Cucumber Changes:
Update page object to use new elements
Thursday, October 3, 13
Examples element :slices_field, "input[name='estimate[slice_count]']" element :beers_field, "input[name='estimate[beer_count]']"
def hunger_level=(hunger) load unless displayed? slices_per_person = case hunger when 'full' then 1 when 'hungry' then 2 when 'starving' then 3 end slices_field.set slices_per_person end
def thirst_level=(thirst) load unless displayed? beer_count = case thirst when 'none' then 0 when 'like' then 2 when 'love' then 3 end beers_field.set beer_count end
Thursday, October 3, 13
Examplesclass NewEstimatePage < SitePrism::Page
element :hunger_select, "select[name='estimate[slice_count]']" element :thirst_select, "select[name='estimate[beer_count]']"
def hunger_level=(hunger) load unless displayed? hunger_select.select hunger end
def thirst_level=(thirst) load unless displayed? thirst_select.select case thirst when 'like' then 'thirsty' when 'love' then 'extremely thirsty' else 'none' end endend
Thursday, October 3, 13
Examples
Thursday, October 3, 13
MORE Changes
Thursday, October 3, 13
Product Manager:
“We’re adding a registra9on feature.”
“Entering the number of guests is redundant.”
“We’ll count the guests in the database.”
Thursday, October 3, 13
Application Changes:Create Guest model, with db migra9on
Add guest list view with add guest form
Add guests controller
Change routes to add guest routes
Change es9mate form view
Change es9mate controller
Thursday, October 3, 13
Cucumber Changes:
Change the step that sets up the number of guests.
Thursday, October 3, 13
Examples
Given /^there are (\d+) guests expected$/ do |guest_count| guests_expected guest_countend
Thursday, October 3, 13
Examples
Given /^there are (\d+) guests expected$/ do |guest_count| guest_count.times{ Fabricate(:guest) }end
Thursday, October 3, 13
Alternative
Change the implementa9on of the method the step calls.
Thursday, October 3, 13
Alternative
def guests_expected guest_count Site.new_estimate_page.guests_expected = count end
Thursday, October 3, 13
Alternative
def guests_expected guest_count guest_count.times{ Fabricate(:guest) } end
Thursday, October 3, 13
Alternative
module DatabaseHelper def guests_expected guest_count guest_count.times{ Fabricate(:guest) } endend
Thursday, October 3, 13
Alternative# support/helpers/database_helper.rb
module DatabaseHelper def guests_expected guest_count guest_count.times{ Fabricate(:guest) } endend
# support/helpers/helper_setup.rb
require_relative 'web_helper'require_relative 'database_helper'
World(WebHelper)World(DatabaseHelper)
Thursday, October 3, 13
Examples
Thursday, October 3, 13
Examples
Thursday, October 3, 13
Examples
Thursday, October 3, 13
platform changes
Thursday, October 3, 13
Examplesmodule PersonalAssistantHelper def general_hunger_level hunger PersonalAssistant.tell "Guests will be #{hunger}" end
def general_thirst_level thirst PersonalAssistant.tell "Guests will #{thirst} beer" unless thirst == 'none' end
def submit_request_for_estimate PersonalAssistant.ask "How much do I need?" end
def verify_pizzas_needed num_pies expect(PersonalAssistant.guess_pizza).to eq(num_pies) end
def verify_beer_needed beer_text expect(PersonalAssistant.guess_beer).to eq(beer_text) endend
Thursday, October 3, 13
SUMMARY
Keep implementa9on details in one place
Use objects to model the system
Use abstrac9on levels to scope changes
Express your intent throughout the code
Thursday, October 3, 13
QUESTIONS?
Thursday, October 3, 13
RESOURCESSource Code:
github.com/maFvanhorn/pizza_beergithub.com/natritmeyer/site_prism
Further Reading:mar9nfowler.com/bliki/PageObject.htmlblog.maFwynne.net/2012/11/20/tdd-‐vs-‐bdddannorth.net/2012/05/31/bdd-‐is-‐like-‐tdd-‐ifwww.elabs.se/blog/15-‐you-‐re-‐cuking-‐it-‐wronggojko.net/2013/09/30/wri9ng-‐as-‐a-‐user-‐does-‐not-‐make-‐it-‐a-‐user-‐story
Contact Me:web: maFvanhorn.comgithub: maFvanhorntwiFer: @nycplayeremail: mvanhorn@newrelic or [email protected]
Thursday, October 3, 13