13. 4 Main Purposes of Testing
1. Assertion
We “exercise” production code with test
code to ensure it’s behavior (test harness)
14.
15. class Person
attr_accessor :interests
def initialize
@interests = []
end
end
16. class Person
attr_accessor :interests
def initialize
@interests = []
end
end
describe Person do
before(:each) { @alex = Person.new }
it "should have interests" do
@alex.should respond_to :interests
end
end
17. class Person
attr_accessor :interests
def initialize
@interests = []
end
end
describe Person do
before(:each) { @alex = Person.new }
it "should have interests" do
@alex.should respond_to :interests
end
end
40. BDD
“I found the shift from thinking in tests to thinking in
behaviour so profound that I started to refer to TDD as
BDD, or behaviour- driven development.”
-- Dan North, http://dannorth.net/introducing-bdd
41. BDD
BDD is a different way of thinking about testing
43. A cucumber feature
Scenario: Creating a new owner
When I go to the new owner page # create new action and routes
And I fill in the following:
| Name | Alex’s Properties |
| Phone | 111-222-3333 |
| Email | email@alex.com |
And I press "Create"
Then I should see "Owner was successfully created."
44. A cucumber feature
Scenario: Creating a new owner
When I go to the new owner page
And I fill in the following:
| Name | Alex’s Properties | # database migrations, model validations
| Phone | 111-222-3333 | # create action
| Email | email@alex.com |
And I press "Create"
Then I should see "Owner was successfully created."
45. A cucumber feature
Scenario: Creating a new owner
When I go to the new owner page
And I fill in the following:
| Name | Alex’s Properties |
| Phone | 111-222-3333 |
| Email | email@alex.com |
And I press "Create"
Then I should see "Owner was successfully created."
# create show action to view new owner
46. Writing Tests vs Describing Behavior
BDD keeps us grounded in business requirements
47. Writing Tests vs Describing Behavior
The concept of BDD is tightly coupled to agile thinking
48. Writing Tests vs Describing Behavior
Only do what’s necessary to accomplish the
business requirement
49. Writing Tests vs Describing Behavior
Then gather feedback and refactor
50. Writing Tests vs Describing Behavior
So we write more more high-value code
51. Writing Tests vs Describing Behavior
And less code gets thrown out
52. Writing Tests vs Describing Behavior
The difference is subtle, but significant.
63. def display_address
[address1, address2].reject { |address| address.nil? }.join(", ")
end
it "should display the first part of the street address if it exists" do
@demographic.display_address.should == "123 Main St"
end
it "should display the first and second street addresses if they exist" do
@demographic.address2 = "Apt 2"
@demographic.display_address.should == "123 Main St, Apt 2"
end
it "should display an empty string if neither exist" do
@demographic.address1 = @demographic.address2 = nil
@demographic.display_address.should == ""
end
69. describe User, '#new' do
before :each do
@user = Factory.build(:user)
end
it 'should send an email when saved' do
lambda {
@user.save
}.should change(ActionMailer::Base.deliveries, :size)
end
end
70. describe User, '#new' do
before :each do
@user = Factory.build(:user)
end
it 'should send an email when saved' do
lambda {
@user.save
}.should change(ActionMailer::Base.deliveries, :size)
end
end
Intended behavior is pretty clear
71. it "should not display the same clinic more than once" do
clinics = @user.find_all_clinics_by_practice(@user.practice)
clinics.should == clinics.uniq
end
72. it "should not display the same clinic more than once" do
clinics = @user.find_all_clinics_by_practice(@user.practice)
clinics.should == clinics.uniq
end
Intended behavior is pretty clear
75. describe Post do
it "should successfully save" do
@post = Post.new :title => "Title", :body => "profound words..."
@post.save.should == true
@post.permalink.should_not be_blank
@post.comments.should_not be_nil
@post.comments.should respond_to :build
end
end
76. X
describe Post do
it "should successfully save" do
@post = Post.new :title => "Title", :body => "profound words..."
@post.save.should == true
@post.permalink.should_not be_blank
@post.comments.should_not be_nil
@post.comments.should respond_to :build
end
end
This is no good.
77. describe Post do
it "should successfully save" do
@post = Post.new :title => "Title", :body => "profound words..."
@post.save.should == true
@post.permalink.should_not be_blank
@post.comments.should_not be_nil
@post.comments.should respond_to :build
end
end
78. describe Post do
it "should successfully save" do
@post = Post.new :title => "Title", :body => "profound words..."
@post.save.should == true
@post.permalink.should_not be_blank
@post.comments.should_not be_nil
@post.comments.should respond_to :build
end
end
Too much is being tested.
79. describe Post do
it "should successfully save" do
@post = Post.new :title => "Title", :body => "profound words..."
@post.save.should == true
@post.permalink.should_not be_blank
@post.comments.should_not be_nil
@post.comments.should respond_to :build
end
end
More than one object being tested
Too much is being tested.
80. A better solution
describe Post do
before :each do
@post = Post.new :title => "Title", :body => "profound words..."
end
it "should successfully save" do
@post.save.should == true
end
it "should create a permalink" do
@post.permalink.should_not be_blank
end
it "should create a comments collection" do
@post.comments.should_not be_nil
end
end
85. Good Testing Principles
• Descriptive naming
• Single Responsibility
• Short test methods => readable
• Prefer conciseness and readability over DRY
86. A quick aside...
If you feel like reading some really beautiful
communicative code, take a look at sinatra.
github.com/sinatra/sinatra
99. To refactor is to improve the design of code
without changing it’s behavior
100. def update
@medical_history = @patient.medical_histories.find(params[:id])
@medical_history.taken_date = Date.today if @medical_history.taken_date.nil?
success = true
conditions = @medical_history.existing_conditions_medical_histories.find(:all)
params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
respond_to do |format|
if @medical_history.update_attributes(params[:medical_history]) && success
format.html {
flash[:notice] = 'Medical History was successfully updated.'
redirect_to( patient_medical_histories_url(@patient) ) }
format.xml { head :ok }
format.js # update.rjs
else
format.html { render :action => "edit" }
format.xml { render :xml => @medical_history.errors, :status => :unprocessable_entity }
format.js { render :text => "Error Updating History", :status => :unprocessable_entity}
end
end
end
101. Characterization Testing
Lot’s of violations here:
1. Fat model, skinny controller
2. Single responsibility
3. Not modular
4. Really hard to fit on a slide
102. This is what I want to focus on
params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
103. Characterization Testing
I want to refactor in the following ways:
1. Push this code into the model
2. Extract logic into separate methods
104. Characterization Testing
We need to write some characterization
tests and get this action under test
106. describe MedicalHistoriesController, "PUT update" do
before :each do
@user = Factory(:practice_admin)
@patient = Factory(:patient_with_medical_histories, :practice => @user.practice)
@medical_history = @patient.medical_histories.first
@condition1 = Factory(:existing_condition)
@condition2 = Factory(:existing_condition)
stub_request_before_filters @user, :practice => true, :clinic => true
params = { :conditions =>
{ "condition_#{@condition1.id}" => "true",
"condition_#{@condition2.id}" => "true" },
:id => @medical_history.id,
:patient_id => @patient.id
}
put :update, params
end
it "should successfully save a collection of conditions" do
@medical_history.existing_conditions.should include @condition1
@medical_history.existing_conditions.should include @condition2
end
end
107. Start by extracting this line
params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
108. describe StringExtensions, "#extract_last_number" do
it "should respond to #extract_last_number" do
"test_string_123".should respond_to :extract_last_number
end
it "should return numbers included in a string" do
"condition_123_yes".extract_last_number.should == 123
end
it "should return the last number included in a string" do
"condition_777_123_yes".extract_last_number.should == 123
end
it "should return nil if there are no numbers in a string" do
"condition_yes".extract_last_number.should == nil
end
it "should return nil if the string is empty" do
"".extract_last_number.should == nil
end
it "should return a number" do
"condition_234_yes".extract_last_number.should be_instance_of Fixnum
end
end
109. module StringExtensions
def extract_last_number
result = self.to_s.scan(/(d+)/).last
result ? result.last.to_i : nil
end
end
String.send :include, StringExtensions
110. module StringExtensions
def extract_last_number
result = self.to_s.scan(/(d+)/).last
result ? result.last.to_i : nil
end
end
String.send :include, StringExtensions
111. i.e. conditions.find(:all)
params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
112. params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
113. def update_conditions(conditions_param = {})
conditions_param.each_pair do |key, value|
condition_id = key.extract_last_number # is extended in lib/reg_exp_helpers.rb
create_or_update_condition(condition_id, value)
end
end
private
def create_or_update_condition(condition_id, value)
condition = existing_conditions_medical_histories.find_by_existing_condition_id(condition_id)
condition.nil? ? create_condition(condition_id, value) : update_condition(condition, value)
end
def create_condition(condition_id, value)
existing_conditions_medical_histories.create(
:existing_condition_id => condition_id,
:has_condition => value
)
end
def update_condition(condition, value)
condition.update_attribute(:has_condition, value) unless condition.has_condition == value
end
114. def update
@medical_history = @patient.medical_histories.find(params[:id])
@medical_history.taken_date = Date.today if @medical_history.taken_date.nil?
#
# edit the existing conditions
#
success = true
conditions = @medical_history.existing_conditions_medical_histories.find(:all)
@medical_history.update_conditions(params[:conditions])
respond_to do |format|
if @medical_history.update_attributes(params[:medical_history]) && success
format.html {
flash[:notice] = 'Medical History was successfully updated.'
redirect_to( patient_medical_histories_url(@patient) ) }
format.xml { head :ok }
format.js # update.rjs
else
format.html { render :action => "edit" }
format.xml { render :xml => @medical_history.errors, :status
=> :unprocessable_entity }
format.js { render :text => "Error Updating History", :status
=> :unprocessable_entity}
end
end
115. We started with this, in the controller...
params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
116. We finished with this, in the model
def update_conditions(conditions_param = {})
conditions_param.each_pair do |key, value|
create_or_update_condition(key.extract_last_number, value)
end
end
118. We started with this, in the controller...
params[:conditions].each_pair do |key,value|
condition_id = key.to_s[10..-1].to_i
condition = conditions.detect {|x| x.existing_condition_id == condition_id }
if condition.nil?
# create the existing_conditions_medical_conditions
success = @medical_history.existing_conditions_medical_histories.create(
:existing_condition_id => condition_id, :has_condition => value) && success
elsif condition.has_condition != value
success = condition.update_attribute(:has_condition, value) && success
end
end
119. Legacy Code
We can identify a few common problems
when working with untested legacy code.
132. We want to alter
some behavior here Calling Code
index.erb
PatientsControllerV2#index
PatientsController#index Known calls
show.erb
phantom ajax call Unknown calls
133. We want to alter
some behavior here Calling Code
index.erb
PatientsControllerV2#index Known calls
show.erb
PatientsController#index phantom ajax call Unknown calls
134. class PatientsController < ApplicationController
def find_by_name
@patients = []
if params[:name] then
@patients = @practice.patients.search(params[:name], nil)
elsif params[:last_name] and params[:first_name]
@patients = @practice.patients.find( :all,
:conditions => [ 'last_name like ? and first_name like ?', params[:last_name] + '%',
params[:first_name] + '%' ],
:order => 'last_name, first_name' )
end
respond_to do |format|
format.json { render :json => @patients.to_json(:methods => [
:full_name_last_first, :age, :home_phone, :prefered_phone ]),
:layout => false
}
end
end
def index
@patients = @practice.patients.search(params[:search], params[:page])
respond_to do |format|
format.html { render :layout => 'application' } # index.html.erb
format.xml { render :xml => @patients }
format.json { render :json => @patients.to_json(:methods =>
[ :full_name_last_first, :age ]), :layout => false }
end
end
end
135. class V2::PatientsController < ApplicationController
def index
@patients = @practice.patients.all_paginated([], [], params[:page])
respond_to do |format|
format.html { render :layout => 'application', :template => 'patients/index' }
end
end
end
136. class PatientsController < ApplicationController
def index
logger.warn "DEPRECATION WARNING! Please use /v2/patients"
HoptoadNotifier.notify(
:error_class => "DEPRECATION",
:error_message => "DEPRECATION WARNING!: /patients/index invoked",
:parameters => params
)
@patients = @practice.patients.search(params[:search], params[:page])
respond_to do |format|
format.html { render :layout => 'application' } # index.html.erb
format.xml { render :xml => @patients }
format.json { render :json => @patients.to_json(:methods =>
[ :full_name_last_first, :age ]), :layout => false }
end
end
end
This talk is about writing tests. First and foremost, testing code is about ensuring behavior. However, I&#x2019;m interested in the less obvious benefits and purposes of testing. And more importantly, I&#x2019;m interested in how we can apply that to make our lives as developers easier.
In &#x201C;Working with Legacy Code&#x201D;, Michael Feathers talks about &#x201C;exercising&#x201D; code in a test harness.
Michael Feather&#x2019;s refers to what we call test frameworks (xUnit, rspec) as a &#x201C;test harness&#x201D;. In this case, rspec.
Michael Feather&#x2019;s refers to what we call test frameworks (xUnit, rspec) as a &#x201C;test harness&#x201D;. In this case, rspec.
Michael Feather&#x2019;s refers to what we call test frameworks (xUnit, rspec) as a &#x201C;test harness&#x201D;. In this case, rspec.
BDD is clear about the fact that we should work outside-in, while performing the necessary functional or unit testing along the way. TDD is more general, and simply says that we should write our tests first to drive production code.
Rick Bradley talks about this in his talk from Hoedown 08. He makes the point that you can usually tell test-driven code from not test-driven code. It tends to be simpler, reusable and more modular. In other words, easy to test.
Single Responsibility
Easier to refactor
Fewer bugs in production
i.e. In other words, when testing, you should focus on describing what your code should do, not how it should do it.
i.e. In other words, when testing, you should focus on describing what your code should do, not how it should do it.
i.e. In other words, when testing, you should focus on describing what your code should do, not how it should do it.
Among many other things, BDD is a different way of thinking about testing.
Every step of this feature represents some behavior that I need to implement. So, if I write these steps incrementally, then each step can literally drive a step of the design. Cucumber is tightly linked to the idea of BDD because of how it allows you to describe your behavior.
Every step of this feature represents some behavior that I need to implement. So, if I write these steps incrementally, then each step can literally drive a step of the design. Cucumber is tightly linked to the idea of BDD because of how it allows you to describe your behavior.
Every step of this feature represents some behavior that I need to implement. So, if I write these steps incrementally, then each step can literally drive a step of the design. Cucumber is tightly linked to the idea of BDD because of how it allows you to describe your behavior.
If you&#x2019;re confused whether I&#x2019;m talking about agile development or BDD, the answer is both, because the principles are almost identical.
In other words, we waste less time writing code that we&#x2019;ll never actually need
This leads us nicely into tests as a communication tool. Obviously, cucumber features can be a very effective communication tools because they&#x2019;re written is plain english, and so they very clearly communicate both the programmer&#x2019;s and the customer&#x2019;s intent.
Cucumber is obviously a great example of this between customer and programmer
I find that the author is often me.
The display_address method is not quite so clear what it is trying to accomplish. Tests can help. After reading the tests, it&#x2019;s pretty clear what the display_address method is doing.
The display_address method is not quite so clear what it is trying to accomplish. Tests can help. After reading the tests, it&#x2019;s pretty clear what the display_address method is doing.
Especially cucumber.
The description tells me exactly what is going on when a new user gets saved.
The description tells me exactly what is going on when a new user gets saved.
Focus is lost when we have to pour through poorly written, named and organized tests, especially if the production code is not particularly pretty.
Ideally, each test method should target the assertion of one thing.
Ideally, each test method should target the assertion of one thing.
Ideally, each test method should target the assertion of one thing.
Ideally, each test method should target the assertion of one thing.
Ideally, each test method should target the assertion of one thing.
So to recap, for test code to be an effective communication tool
This one is really important. Always adhere to the single responsibility principle by writing targeted and concise test methods that test one thing at a time.
Notice that there is no mention of isolation.
I point this out because the term &#x201C;refactor&#x201D; is thrown about loosely, but for purposes of this talk, this definition is important.
This thing was not fun to test.
This thing was not fun to test.
Rick Bradley described characterization testing in this manner as putting up scaffolds around your code.
Rick Bradley described characterization testing in this manner as putting up scaffolds around your code.
These are the situations where code reading becomes a very necessary part of the process.
This is a refactored version of a few slides back. This is much more manageable, and we increased it&#x2019;s testability by splitting up some of the logic. This first part just handles the looping and extracting a number, albeit, in a rather ugly-ish way. We&#x2019;ve offloaded the logic behind whether to create or update somewhere else. Even better the characterization tests we wrote to cover the initial iteration still apply here and validate that this is still working as expected.
Obviously, in the ideal scenario, you know all the places from which a particular API is being invoked, and resolving dependency issues with calling code is trivial. This may be the case if you have a well-organized, modular codebase.
Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.
Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.
Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.
Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.