Object-Oriented BDD w/ Cucumber by Matt van Horn

14,891 views

Published on

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/

Published in: Technology

Object-Oriented BDD w/ Cucumber by Matt van Horn

  1. 1. OBJECT ORIENTED BDD with CUCUMBER Thursday, October 3, 13
  2. 2. 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
  3. 3. New Relic CI Thursday, October 3, 13
  4. 4. New Relic CI Github,  TDDium,  Jenkins Feature  branches,  Pull  Requests Automa9c  builds,  Code  reviews Automated  deploys,  but  some  manual  control Thursday, October 3, 13
  5. 5. New Relic CI We  deploy  3x  per  day Automated  tes9ng  is  part  of  the  process However... Thursday, October 3, 13
  6. 6. 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
  7. 7. 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
  8. 8. Behavior Driven development Thursday, October 3, 13
  9. 9. Behavior Driven development BDD  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
  10. 10. Behavior Driven Development “BDD  describes  TDD  done  well” -­‐  MaF  Wynne Thursday, October 3, 13
  11. 11. Behavior Driven Development “Can  you  give  me  some  examples  (of  using  it)?” vs. “What  are  the  requirements  (for  implemen9ng  it)?” Thursday, October 3, 13
  12. 12. Outside In Start  with  a  conversa9on Determine  the  business  value Provide  examples  of  use Thursday, October 3, 13
  13. 13. Outside In  User  Interface Browser Views Controllers Models Thursday, October 3, 13
  14. 14. Outside In  User  Interface Browser Views Controllers Models Cucumber RSpec Thursday, October 3, 13
  15. 15. Outside In Red,  Green,  REFACTOR Outer  loop:  Features,   stories,  scenarios Inner  loop:  Unit  tests,   classes,  methods Cucumber Thursday, October 3, 13
  16. 16. Red,  Green,  REFACTOR Outer  loop:  Features,   stories,  scenarios Inner  loop:  Unit  tests,   classes,  methods Outside In Cucumber RSpec Thursday, October 3, 13
  17. 17. Outside In Cucumber RSpecRed,  Green,  REFACTOR Outer  loop:  Features,   stories,  scenarios Inner  loop:  Unit  tests,   classes,  methods Thursday, October 3, 13
  18. 18. Outside In Cucumber RSpecRed,  Green,  REFACTOR Outer  loop:  Features,   stories,  scenarios Inner  loop:  Unit  tests,   classes,  methods Thursday, October 3, 13
  19. 19. BDD TOOLS Cucumber,  RSpec,  FitNesse Gherkin Spinach,  Turnip,  Steak,  Filet Capybara,  Wa9r,  WebRAT Selenium,  Webkit,  PhantomJS,  Rack::Test Thursday, October 3, 13
  20. 20. Cucumber Complaints Don’t  need  English,  code  is  fine Doesn’t  save  developer  9me BriFle  tests,  needing  constant  maintenance Thursday, October 3, 13
  21. 21. You’re DoING IT WRONG Thursday, October 3, 13
  22. 22. Cucumber Done right Minimizes  miscommunica9on Hides  implementa9on  details Provides  robust  regression  tests Communicates  inten9ons Thursday, October 3, 13
  23. 23. 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
  24. 24. Object ORiented Cucumber Thursday, October 3, 13
  25. 25. Object ORiented Cucumber Keep  data  and  behavior  together Hide  implementa9on  details Send  messages  to  accomplish  tasks Thursday, October 3, 13
  26. 26. Page Object URLs UI Elements Page Sections Domain Action Methods Browser Driver Thursday, October 3, 13
  27. 27. 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
  28. 28. Site Prism Open  source  Ruby  gem   Provides  simple  DSL  for  page  Objects Exposes  Capybara  nodes Thursday, October 3, 13
  29. 29. Technique Plain  English,  not  code  (or  pseudo-­‐code) Domain  concepts,  not  implementa9on  details Focus  on  the  value,  not  the  incidentals Thursday, October 3, 13
  30. 30. 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
  31. 31. “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
  32. 32. 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
  33. 33. Examples Feature: 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
  34. 34. StepDefinitions Use  methods  that  relate  to  the  domain Avoid  nes9ng  steps Mostly  black  box Some  white  box  can  be  very  useful Thursday, October 3, 13
  35. 35. Examples Given(/^there are (d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_count end Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_level end When 'I ask how much to order' do Site.new_estimate_page.request_estimate end 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
  36. 36. Examples Given(/^there are (d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_count end Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_level end When 'I ask how much to order' do Site.new_estimate_page.request_estimate end 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
  37. 37. 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 end end Thursday, October 3, 13
  38. 38. Examples class 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
  39. 39. Examples class 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
  40. 40. Examples class 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
  41. 41. 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 end end Thursday, October 3, 13
  42. 42. DEMO Thursday, October 3, 13
  43. 43. DEMO Thursday, October 3, 13
  44. 44. Changes Thursday, October 3, 13
  45. 45. Product Manager: “We  should  es9mate  beer  as  well” “It  should  work  the  same  way  as  pizza” Thursday, October 3, 13
  46. 46. Application Changes: Change  es9mate  model Change  form  view Change  es9mate  view Change  es9mate  view  helper Thursday, October 3, 13
  47. 47. 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
  48. 48. 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
  49. 49. Examples Given(/^the guests (like|love) beer$/) do |thirst| Site.new_estimate_page.thirst_level = thirst end 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
  50. 50. Examples class 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 end end Thursday, October 3, 13
  51. 51. Examples Thursday, October 3, 13
  52. 52. Examples Thursday, October 3, 13
  53. 53. Refactor Thursday, October 3, 13
  54. 54. Examples Given(/^there are (d+) guests expected$/) do |guest_count| Site.new_estimate_page.guests_expected = guest_count end Given(/^the guests are (full|hungry|starving)$/) do |hunger_level| Site.new_estimate_page.hunger_level = hunger_level end When 'I ask how much to order' do Site.new_estimate_page.request_estimate end 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
  55. 55. HELPER Modules Helpers  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
  56. 56. Examples module 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) end end World(WebHelper) Thursday, October 3, 13
  57. 57. ExamplesTransform /(d+)/ do |num| num.to_i end Given /^there are (d+) guests expected$/ do |guest_count| guests_expected guest_count end Given /^the guests are (full|hungry|starving)$/ do |hunger| general_hunger_level hunger end Given /^the guests (like|love) beer$/ do |thirst| general_thirst_level thirst end When 'I ask how much to order' do submit_request_for_estimate end Then(/^I will know I need to buy (d+) pizza pies$/) do |pie_count| verify_pizzas_needed pie_count end Then(/^I will know I need to buy ((?:d+ cases?)?(?: and )?(?:d+ six-packs?)? of beer) $/) do |beer_text| verify_beer_needed beer_text end Thursday, October 3, 13
  58. 58. 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
  59. 59. Application Changes: Change  form  view  to  use  different  widgets Thursday, October 3, 13
  60. 60. Cucumber Changes: Update  page  object  to  use  new  elements Thursday, October 3, 13
  61. 61. 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
  62. 62. Examples class 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 end end Thursday, October 3, 13
  63. 63. Examples Thursday, October 3, 13
  64. 64. MORE Changes Thursday, October 3, 13
  65. 65. 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
  66. 66. 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
  67. 67. Cucumber Changes: Change  the  step  that  sets  up  the  number  of   guests. Thursday, October 3, 13
  68. 68. Examples Given /^there are (d+) guests expected$/ do |guest_count| guests_expected guest_count end Thursday, October 3, 13
  69. 69. Examples Given /^there are (d+) guests expected$/ do |guest_count| guest_count.times{ Fabricate(:guest) } end Thursday, October 3, 13
  70. 70. Alternative Change  the  implementa9on  of  the  method   the  step  calls. Thursday, October 3, 13
  71. 71. Alternative def guests_expected guest_count Site.new_estimate_page.guests_expected = count end Thursday, October 3, 13
  72. 72. Alternative def guests_expected guest_count guest_count.times{ Fabricate(:guest) } end Thursday, October 3, 13
  73. 73. Alternative module DatabaseHelper def guests_expected guest_count guest_count.times{ Fabricate(:guest) } end end Thursday, October 3, 13
  74. 74. Alternative # support/helpers/database_helper.rb module DatabaseHelper def guests_expected guest_count guest_count.times{ Fabricate(:guest) } end end # support/helpers/helper_setup.rb require_relative 'web_helper' require_relative 'database_helper' World(WebHelper) World(DatabaseHelper) Thursday, October 3, 13
  75. 75. Examples Thursday, October 3, 13
  76. 76. Examples Thursday, October 3, 13
  77. 77. Examples Thursday, October 3, 13
  78. 78. platform changes Thursday, October 3, 13
  79. 79. 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) end end Thursday, October 3, 13
  80. 80. 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
  81. 81. QUESTIONS? Thursday, October 3, 13
  82. 82. RESOURCES Source  Code: github.com/maFvanhorn/pizza_beer github.com/natritmeyer/site_prism Further  Reading: mar9nfowler.com/bliki/PageObject.html blog.maFwynne.net/2012/11/20/tdd-­‐vs-­‐bdd dannorth.net/2012/05/31/bdd-­‐is-­‐like-­‐tdd-­‐if www.elabs.se/blog/15-­‐you-­‐re-­‐cuking-­‐it-­‐wrong gojko.net/2013/09/30/wri9ng-­‐as-­‐a-­‐user-­‐does-­‐not-­‐make-­‐it-­‐a-­‐user-­‐story Contact  Me: web:  maFvanhorn.com github:  maFvanhorn twiFer:  @nycplayer email:  mvanhorn@newrelic  or  maFvanhorn@gmail.com Thursday, October 3, 13

×