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/
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
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. New Relic CI
We
deploy
3x
per
day
Automated
tes9ng
is
part
of
the
process
However...
Thursday, October 3, 13
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. 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
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
22. Cucumber Done right
Minimizes
miscommunica9on
Hides
implementa9on
details
Provides
robust
regression
tests
Communicates
inten9ons
Thursday, October 3, 13
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
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. Site Prism
Open
source
Ruby
gem
Provides
simple
DSL
for
page
Objects
Exposes
Capybara
nodes
Thursday, October 3, 13
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. 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. “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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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
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. 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. 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. 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
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. 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. 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. 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. 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
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. 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
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. 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
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. 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