object-oriented bdd w/ cucumber by matt van horn

Post on 28-Nov-2014

10.303 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

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  maFvanhorn@gmail.com

Thursday, October 3, 13

top related