At BeBanjo we develop web applications to manage all aspects of TV operations. Our development process is driven by User Stories, is divided in short iterations, implemented in pairs, integrating continuously, specifying behaviour and we use Ruby on Rails.
In this talk we want to share with the community how we do it; we will talk about technologies like Cucumber, RSpec, Selenium, Webrat or Cruisecontrol, but also about behaviour driven development, pair programming, beautiful code and above else about why we choose these practices over others, what business value they provide and why we think they make us more efficient and ultimately, happier.
All of this not from a theoric point of view, but rather straight from the trenches, sharing our experience and our itches, internal debates that we constantly have in pursuit of excellence.
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Bdd From The Trenches
1. BDD FROM THE
Luismi Cavallé
TRENCHES Jorge Gómez Sancha
BeBanjo
2. As a [Role]
I want [Feature]
So that [Benefit]
Scenario = Acceptance Criteria
Given [Context]
And [Some more Context]
When [Event]
And [Some other Event]
Then [Outcome]
And [Another Outcome]
3.
4.
5.
6.
7.
8. Given [Context] Planteamiento
When [Event] Nudo
Then [Outcome] Desenlace
9.
10.
11. User Stories
=
Great way to communicate functionality
22. Story: User sees timeline for a title
As a user,
I want to see a timeline of all the events associated to a title,
So that I have access to its full history
Scenario: An entry in the title timeline gets created when scheduled
Given Telefonica operates VOD services Imagenio
And a user Borat from the company Telefonica
And Pedro scheduled the title Spaceballs on 15-03-2008 in Imagenio
When Borat logs in
And Borat goes to the title's timeline page for title Spaceballs
Then he should see an entry 'Scheduled by Borat' on March 15, 2008
36. # /bebanjo_app/features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt'
Then he should be redirected to the home page
39. $ cd bebanjo_app
$ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt' [PENDING]
When Robert logs in entering the password 'r0b3rt' [PENDING]
Then he should be redirected to the home page [PENDING]
45. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt' [FAILED]
uninitialized constant User (NameError)
When Robert logs in entering the password 'r0b3rt' [PENDING]
Then he should be redirected to the home page [PENDING]
46. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt' [FAILED]
uninitialized constant User (NameError)
When Robert logs in entering the password 'r0b3rt' [PENDING]
Then he should be redirected to the home page [PENDING]
50. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt' [PENDING]
Then he should be redirected to the home page [PENDING]
58. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt' [FAILED]
No route matches quot;/session/newquot; with {:method=>:get} (RoutingError)
Then he should be redirected to the home page [PENDING]
59. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt' [FAILED]
No route matches quot;/session/newquot; with {:method=>:get} (RoutingError)
Then he should be redirected to the home page [PENDING]
70. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt'
Then he should be redirected to the home page [PENDING]
73. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt'
Then he should be redirected to the home page [FAILED]
expected redirect to quot;/quot;, got no redirect (ExpectationNotMetError)
74. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt'
Then he should be redirected to the home page [FAILED]
expected redirect to quot;/quot;, got no redirect (ExpectationNotMetError)
77. $ script/cucumber features/login.feature
Scenario: User logs in successfully
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'r0b3rt'
Then he should be redirected to the home page
3 steps passed
78. # /bebanjo_app/features/login.feature
Scenario: User logs in with incorrect credentials
Given an existing user Robert with password 'r0b3rt'
When Robert logs in entering the password 'robert'
Then he should not be redirected to the home page
79. $ git commit -a -m “Scenario: User logs in successfully”
80. $ git commit -a -m “Scenario: User logs in successfully”
$ git push origin master
92. # user_steps.rb
Given /^an existing user (.*)$/ do |username|
create_user(:username => username)
end
# example_data.rb (fixture_replacement)
attributes_for :user do |u|
u.username = String.random
u.password = quot;secretquot;
u.password_confirmation = quot;secretquot;
u.company = default_company
end
93. # user_steps.rb
When /^(.*) logs in$/ do |username|
visits quot;/session/newquot;
fills_in quot;Usernamequot;, :with => username
fills_in quot;Passwordquot;, :with => quot;secretquot;
clicks_button quot;Sign inquot;
end
94. # user_steps.rb
Then /^the email of (.*) should be (.*)/ do |username, email|
user = User.find_by_username(username)
user.email.should == email # assert_equal(email, user.email)
end
95. # user_steps.rb
Then /^the email of (.*) should be (.*)/ do |username, email|
user = User.find_by_username(username)
user.email.should == email # assert_equal(email, user.email)
end
Then /^the user should see the title (.*)/ do |title|
response.should have_tag(quot;.titlequot;, title) # assert_select(quot;.titlequot;, title)
end
98. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
99. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
...but as a development tool....
100. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
...but as a development tool....
• it is not easy to debug
101. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
...but as a development tool....
• it is not easy to debug
• it is slow to run
102. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
...but as a development tool....
• it is not easy to debug
• it is slow to run
• it doesn’t encourage good design
103. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
...but as a development tool....
• it is not easy to debug
• it is slow to run
• it doesn’t encourage good design
• it is not comprehensive
104. Acceptance testing is great...
• as a verification tool
• as a bridge between user needs and automated
tests
...but as a development tool....
• it is not easy to debug
• it is slow to run
• it doesn’t encourage good design
• it is not comprehensive
...so we need something more
114. So, how do we spec...
Our Controllers?
67% Coverage
115. So, how do we spec...
Our Controllers?
67% Coverage
Interaction-based unit test
116. So, how do we spec...
Our Controllers?
67% Coverage
Interaction-based unit test
Still discussing if it is worth
117. # tasks_controller_spec.rb
describe quot;POST 'create'quot; do
before(:each) do
@task_attibutes = stub(Hash)
@task = stub(Task)
@tasks.stub!(:create).and_return(@task)
end
def do_post
post :create, :scheduled_title_id => 1, :task => @task_attributes
end
it quot;should get the scheduled title from the current companyquot; do
@scheduled_titles.should_receive(:find).with(quot;1quot;).and_return(@scheduled_title)
do_post
end
it quot;should create the taskquot; do
@tasks.should_receive(:create).with(@task_attributes)
do_post
end
it quot;should redirect to workflowquot; do
do_post
assigns[:task].should == @task
end
end
119. So, how do we spec...
Our Models?
87% Coverage
120. So, how do we spec...
Our Models?
87% Coverage
State-based functional test
121. So, how do we spec...
Our Models?
87% Coverage
State-based functional test
Only the interesting behavior
122. describe ScheduledTitle do
describe quot;late?quot; do
it quot;should be late if begins_at is in the pastquot; do
scheduled_title = new_scheduled_title(:begins_at => Date.today - 3.days)
scheduled_title.should be_late
end
it quot;should not be late if begins_at is in the futurequot; do
scheduled_title = new_scheduled_title(:begins_at => Date.today + 3.days)
scheduled_title.should_not be_late
end
it quot;should not be late if begins_at is todayquot; do
scheduled_title = new_scheduled_title(:begins_at => Date.today)
scheduled_title.should_not be_late
end
end
128. describe User do
it { Factory(:user).should have_many(:projects) }
it { Factory(:user).should belong_to(:company) }
table_has_columns(User, :string, quot;loginquot;)
end
class User
has_many :projects
belongs_to :company
129. describe User do
it { Factory(:user).should have_many(:projects) }
it { Factory(:user).should belong_to(:company) }
table_has_columns(User, :string, quot;loginquot;)
end
class User create_table quot;usersquot;, :force => true do |t|
has_many :projects t.column :login, :string
end
belongs_to :company
130. describe User do
it { Factory(:user).should have_many(:projects) }
it { Factory(:user).should belong_to(:company) }
table_has_columns(User, :string, quot;loginquot;)
end
Structure is not interesting behavior!
class User create_table quot;usersquot;, :force => true do |t|
has_many :projects t.column :login, :string
end
belongs_to :company
131. Progress Ideal Preso
✓ Estimating, Game, Burndown 20 100
✓ Pair Programming 20
75
✓ Acceptance vs Unit 20
50
Upfront Design 10
25
Love 10
0
Productivity 20 40’ 35’ 30’ 25’ 20’ 15’ 10’ 5’ End!
Total 100
Left: 40