SlideShare a Scribd company logo
1 of 137
Testing.has_many
    :purposes
     by Alex Sharp
       @ajsharp
I’m Alex Sharp.
I work for a healthcare startup called OptimisCorp.
We’re based in Pacific Palisades, just north
            of Santa Monica.
We’re hiring.
So if you want to work by the beach in Santa
        Monica, get in touch with me ;)
Brass Tacks


This is a talk about the many purposes and
              benefits of testing.
Brass Tacks


We’ll span the testing horizon, but...
Brass Tacks


We’re going to talk a lot about LEGACY CODE
4 Main Purposes of Testing

 1. Assertion
 2. Design
 3. Communication
 4. Discovery
Purpose #1: Assertion
4 Main Purposes of Testing


 1. Assertion
   We “exercise” production code with test
   code to ensure it’s behavior (test harness)
class Person
  attr_accessor :interests

  def initialize
    @interests = []
  end
end
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
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
Tests are commonly used as a design tool
Or as we call it...
TDD


Test Driven Development
TDD


I love TDD
TDD


I love BDD more
TDD


Why??
TDD


BDD > TDD
TDD


BDD is more clear about how and what to test.
Contrived Example
class TDD
  def when_to_test
    "first"
  end

  def how_to_test
    "??"
  end
end

class BDD < TDD
  def how_to_test
    "outside-in"
  end
end
Why test-first?


I write better code when it’s test-driven
Better like how?


      Simpler
Better like how?


     More modular
Better like how?


    More maintainable
Better like how?


     More stable
Better like how?


     More readable
Better like how?


  MORE BETTER
Purpose #2: Design
Design
Design
Design


We can use tests to design and describe the
behavior of software.
Design


We can use tests to design and describe the
behavior of software.
But not necessarily HOW...
Design

i.e. Behavior Driven Development
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
BDD

BDD is a different way of thinking about testing
BDD

Not in terms of tests, but in terms of behavior
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."
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."
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
Writing Tests vs Describing Behavior




BDD keeps us grounded in business requirements
Writing Tests vs Describing Behavior




The concept of BDD is tightly coupled to agile thinking
Writing Tests vs Describing Behavior




  Only do what’s necessary to accomplish the
            business requirement
Writing Tests vs Describing Behavior




     Then gather feedback and refactor
Writing Tests vs Describing Behavior




   So we write more more high-value code
Writing Tests vs Describing Behavior




       And less code gets thrown out
Writing Tests vs Describing Behavior




    The difference is subtle, but significant.
Purpose #3: Communication
Communication


  Reading code is hard
Communication


Sometimes programmer intent is not so clear...
Communication


Tests can be a really useful communication tool
Communication


Cucumber greatly facilitates communication
   between customer and programmer
Communication


But programmers need to communicate too...
Communication


Problem: Everyone else’s code is crap...
Communication


Tests can help mitigate the WTF factor and
     communicate the author’s intent
def display_address
  [address1, address2].reject { |address| address.nil? }.join(", ")
end
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
Communication


Tests usually communicate business requirements
       much better than production code
Communication


We can facilitate programmer communication by
       using good practices in test code
Communication


So we need some “good testing practices”
To that end...


Well-named tests are important in test code as
         well as production code...
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
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
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
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
Good Testing Conventions


It’s easy to be lazy with naming , but good naming is
                about communication.
Good Testing Conventions


Test methods should be short, concise and targeted
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
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.
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
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.
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.
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
Communication Recap


In test code aim for short, concise and clean methods
Communication Recap


One assertion per test method (if possible)
Communication Recap


   Single Responsibility Principle
Writing good tests makes it much easier to refactor
Good Testing Principles

•   Descriptive naming
•   Single Responsibility
•   Short test methods => readable
•   Prefer conciseness and readability over DRY
A quick aside...


If you feel like reading some really beautiful
communicative code, take a look at sinatra.
         github.com/sinatra/sinatra
WARNING!!!
Prepare yourself; that was the last of the fun stuff.
Software is more than just greenfield projects
Legacy code is part of life.
So let’s talk about how to test it.
But’s first let’s clarify what we mean by “legacy code”
For purposes of this
       talk...

  Legacy code is code without tests.
 - Michael Feathers, Working Effectively with Legacy Code
Purpose #4: Discovery
Generally, we need to do one of two things
when working with legacy code:
1. Refactor
2. Alter behavior
Characterization Testing


 We use characterization tests when we need
  to make changes to untested legacy code
Characterization Testing


 We need to discover what the code is doing
   before we can responsibly change it.
Characterization Testing


Let’s start with a real world refactoring example...
To refactor is to improve the design of code
        without changing it’s behavior
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
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
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
Characterization Testing

 I want to refactor in the following ways:
 1. Push this code into the model
 2. Extract logic into separate methods
Characterization Testing


  We need to write some characterization
    tests and get this action under test
Characterization Testing


   This way we can rely on an automated
     testing workflow to for feedback
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
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
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
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
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
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
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
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
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
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
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
It’s not perfect, but refactoring must be done in small steps
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
Legacy Code


We can identify a few common problems
when working with untested legacy code.
Legacy Code

1. Lack of sane design principles
2. Dependency hell
3. Unknown calling code
Legacy Code


    “...in legacy code, often all bets are off.”

- Michael Feathers, Working Effectively with Legacy Code
Legacy Code


How do we protect against dependencies we
           don’t know about?
Ideal Scenario


In the ideal scenario, you know all the places
from which a particular API is being invoked
Ideal Scenario


Resolving calling code dependencies is trivial
More Likely Scenario


However, this is not likely, especially for larger apps
More Likely Scenario


Unfortunately, much of the discussion around
this issue assumes awareness of calling code
More Likely Scenario


  This is an unfortunate assumption
More Likely Scenario


  Frequent Offender: Rails controllers
Example


Solution: Direct known calling code elsewhere
Example


Treat your app like a public API
Example


Deprecate it gradually
We want to alter
   some behavior here        Calling Code

                                index.erb
PatientsControllerV2#index
 PatientsController#index                        Known calls
                                 show.erb



                             phantom ajax call   Unknown calls
We want to alter
   some behavior here        Calling Code

                                index.erb
PatientsControllerV2#index                       Known calls
                                 show.erb

 PatientsController#index    phantom ajax call   Unknown calls
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
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
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
Further Resources

More Related Content

Similar to Testing Has Many Purposes

BDD style Unit Testing
BDD style Unit TestingBDD style Unit Testing
BDD style Unit TestingWen-Tien Chang
 
Lotusphere 2007 AD505 DevBlast 30 LotusScript Tips
Lotusphere 2007 AD505 DevBlast 30 LotusScript TipsLotusphere 2007 AD505 DevBlast 30 LotusScript Tips
Lotusphere 2007 AD505 DevBlast 30 LotusScript TipsBill Buchan
 
TDD with PhpSpec - Lone Star PHP 2016
TDD with PhpSpec - Lone Star PHP 2016TDD with PhpSpec - Lone Star PHP 2016
TDD with PhpSpec - Lone Star PHP 2016CiaranMcNulty
 
2011-02-03 LA RubyConf Rails3 TDD Workshop
2011-02-03 LA RubyConf Rails3 TDD Workshop2011-02-03 LA RubyConf Rails3 TDD Workshop
2011-02-03 LA RubyConf Rails3 TDD WorkshopWolfram Arnold
 
Don't let your tests slow you down
Don't let your tests slow you downDon't let your tests slow you down
Don't let your tests slow you downDaniel Irvine
 
It's all about behaviour, also in php - phpspec
It's all about behaviour, also in php - phpspecIt's all about behaviour, also in php - phpspec
It's all about behaviour, also in php - phpspecGiulio De Donato
 
Testing survival Guide
Testing survival GuideTesting survival Guide
Testing survival GuideThilo Utke
 
RSpec: What, How and Why
RSpec: What, How and WhyRSpec: What, How and Why
RSpec: What, How and WhyRatan Sebastian
 
Антипаттерны модульного тестирования
Антипаттерны модульного тестированияАнтипаттерны модульного тестирования
Антипаттерны модульного тестированияMitinPavel
 
Code Quality Makes Your Job Easier
Code Quality Makes Your Job EasierCode Quality Makes Your Job Easier
Code Quality Makes Your Job EasierTonya Mork
 
A sweet taste of clean code and software design
A sweet taste of clean code and software designA sweet taste of clean code and software design
A sweet taste of clean code and software designKfir Bloch
 
Building In Quality: The Beauty Of Behavior Driven Development (BDD)
Building In Quality: The Beauty Of Behavior Driven Development (BDD)Building In Quality: The Beauty Of Behavior Driven Development (BDD)
Building In Quality: The Beauty Of Behavior Driven Development (BDD)Synerzip
 

Similar to Testing Has Many Purposes (20)

Good Coding Practices with JavaScript
Good Coding Practices with JavaScriptGood Coding Practices with JavaScript
Good Coding Practices with JavaScript
 
BDD style Unit Testing
BDD style Unit TestingBDD style Unit Testing
BDD style Unit Testing
 
Designing code
Designing codeDesigning code
Designing code
 
Clean code and code smells
Clean code and code smellsClean code and code smells
Clean code and code smells
 
Cucumber & BDD
Cucumber & BDDCucumber & BDD
Cucumber & BDD
 
Lotusphere 2007 AD505 DevBlast 30 LotusScript Tips
Lotusphere 2007 AD505 DevBlast 30 LotusScript TipsLotusphere 2007 AD505 DevBlast 30 LotusScript Tips
Lotusphere 2007 AD505 DevBlast 30 LotusScript Tips
 
TDD with PhpSpec - Lone Star PHP 2016
TDD with PhpSpec - Lone Star PHP 2016TDD with PhpSpec - Lone Star PHP 2016
TDD with PhpSpec - Lone Star PHP 2016
 
2011-02-03 LA RubyConf Rails3 TDD Workshop
2011-02-03 LA RubyConf Rails3 TDD Workshop2011-02-03 LA RubyConf Rails3 TDD Workshop
2011-02-03 LA RubyConf Rails3 TDD Workshop
 
Don't let your tests slow you down
Don't let your tests slow you downDon't let your tests slow you down
Don't let your tests slow you down
 
It's all about behaviour, also in php - phpspec
It's all about behaviour, also in php - phpspecIt's all about behaviour, also in php - phpspec
It's all about behaviour, also in php - phpspec
 
Tdd is not about testing
Tdd is not about testingTdd is not about testing
Tdd is not about testing
 
Testing survival Guide
Testing survival GuideTesting survival Guide
Testing survival Guide
 
SOLID Ruby, SOLID Rails
SOLID Ruby, SOLID RailsSOLID Ruby, SOLID Rails
SOLID Ruby, SOLID Rails
 
RSpec: What, How and Why
RSpec: What, How and WhyRSpec: What, How and Why
RSpec: What, How and Why
 
Антипаттерны модульного тестирования
Антипаттерны модульного тестированияАнтипаттерны модульного тестирования
Антипаттерны модульного тестирования
 
Code Quality Makes Your Job Easier
Code Quality Makes Your Job EasierCode Quality Makes Your Job Easier
Code Quality Makes Your Job Easier
 
A sweet taste of clean code and software design
A sweet taste of clean code and software designA sweet taste of clean code and software design
A sweet taste of clean code and software design
 
Rspec
RspecRspec
Rspec
 
Gateway to Agile: XP and BDD
Gateway to Agile: XP and BDD Gateway to Agile: XP and BDD
Gateway to Agile: XP and BDD
 
Building In Quality: The Beauty Of Behavior Driven Development (BDD)
Building In Quality: The Beauty Of Behavior Driven Development (BDD)Building In Quality: The Beauty Of Behavior Driven Development (BDD)
Building In Quality: The Beauty Of Behavior Driven Development (BDD)
 

More from Alex Sharp

Bldr: A Minimalist JSON Templating DSL
Bldr: A Minimalist JSON Templating DSLBldr: A Minimalist JSON Templating DSL
Bldr: A Minimalist JSON Templating DSLAlex Sharp
 
Bldr - Rubyconf 2011 Lightning Talk
Bldr - Rubyconf 2011 Lightning TalkBldr - Rubyconf 2011 Lightning Talk
Bldr - Rubyconf 2011 Lightning TalkAlex Sharp
 
Mysql to mongo
Mysql to mongoMysql to mongo
Mysql to mongoAlex Sharp
 
Refactoring in Practice - Sunnyconf 2010
Refactoring in Practice - Sunnyconf 2010Refactoring in Practice - Sunnyconf 2010
Refactoring in Practice - Sunnyconf 2010Alex Sharp
 
Refactoring in Practice - Ruby Hoedown 2010
Refactoring in Practice - Ruby Hoedown 2010Refactoring in Practice - Ruby Hoedown 2010
Refactoring in Practice - Ruby Hoedown 2010Alex Sharp
 
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010Alex Sharp
 
Practical Ruby Projects with MongoDB - Ruby Midwest
Practical Ruby Projects with MongoDB - Ruby MidwestPractical Ruby Projects with MongoDB - Ruby Midwest
Practical Ruby Projects with MongoDB - Ruby MidwestAlex Sharp
 
Practical Ruby Projects with MongoDB - MongoSF
Practical Ruby Projects with MongoDB - MongoSFPractical Ruby Projects with MongoDB - MongoSF
Practical Ruby Projects with MongoDB - MongoSFAlex Sharp
 
Practical Ruby Projects With Mongo Db
Practical Ruby Projects With Mongo DbPractical Ruby Projects With Mongo Db
Practical Ruby Projects With Mongo DbAlex Sharp
 
Intro To MongoDB
Intro To MongoDBIntro To MongoDB
Intro To MongoDBAlex Sharp
 
Getting Comfortable with BDD
Getting Comfortable with BDDGetting Comfortable with BDD
Getting Comfortable with BDDAlex Sharp
 

More from Alex Sharp (11)

Bldr: A Minimalist JSON Templating DSL
Bldr: A Minimalist JSON Templating DSLBldr: A Minimalist JSON Templating DSL
Bldr: A Minimalist JSON Templating DSL
 
Bldr - Rubyconf 2011 Lightning Talk
Bldr - Rubyconf 2011 Lightning TalkBldr - Rubyconf 2011 Lightning Talk
Bldr - Rubyconf 2011 Lightning Talk
 
Mysql to mongo
Mysql to mongoMysql to mongo
Mysql to mongo
 
Refactoring in Practice - Sunnyconf 2010
Refactoring in Practice - Sunnyconf 2010Refactoring in Practice - Sunnyconf 2010
Refactoring in Practice - Sunnyconf 2010
 
Refactoring in Practice - Ruby Hoedown 2010
Refactoring in Practice - Ruby Hoedown 2010Refactoring in Practice - Ruby Hoedown 2010
Refactoring in Practice - Ruby Hoedown 2010
 
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
Practical Ruby Projects with MongoDB - Ruby Kaigi 2010
 
Practical Ruby Projects with MongoDB - Ruby Midwest
Practical Ruby Projects with MongoDB - Ruby MidwestPractical Ruby Projects with MongoDB - Ruby Midwest
Practical Ruby Projects with MongoDB - Ruby Midwest
 
Practical Ruby Projects with MongoDB - MongoSF
Practical Ruby Projects with MongoDB - MongoSFPractical Ruby Projects with MongoDB - MongoSF
Practical Ruby Projects with MongoDB - MongoSF
 
Practical Ruby Projects With Mongo Db
Practical Ruby Projects With Mongo DbPractical Ruby Projects With Mongo Db
Practical Ruby Projects With Mongo Db
 
Intro To MongoDB
Intro To MongoDBIntro To MongoDB
Intro To MongoDB
 
Getting Comfortable with BDD
Getting Comfortable with BDDGetting Comfortable with BDD
Getting Comfortable with BDD
 

Recently uploaded

08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking MenDelhi Call girls
 
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfThe Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfEnterprise Knowledge
 
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
04-2024-HHUG-Sales-and-Marketing-Alignment.pptxHampshireHUG
 
Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024The Digital Insurer
 
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...Neo4j
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024The Digital Insurer
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking MenDelhi Call girls
 
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProduct Anonymous
 
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...apidays
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonAnna Loughnan Colquhoun
 
Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)wesley chun
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationMichael W. Hawkins
 
presentation ICT roal in 21st century education
presentation ICT roal in 21st century educationpresentation ICT roal in 21st century education
presentation ICT roal in 21st century educationjfdjdjcjdnsjd
 
Artificial Intelligence: Facts and Myths
Artificial Intelligence: Facts and MythsArtificial Intelligence: Facts and Myths
Artificial Intelligence: Facts and MythsJoaquim Jorge
 
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptxEIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptxEarley Information Science
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerThousandEyes
 
Partners Life - Insurer Innovation Award 2024
Partners Life - Insurer Innovation Award 2024Partners Life - Insurer Innovation Award 2024
Partners Life - Insurer Innovation Award 2024The Digital Insurer
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...Martijn de Jong
 
Boost PC performance: How more available memory can improve productivity
Boost PC performance: How more available memory can improve productivityBoost PC performance: How more available memory can improve productivity
Boost PC performance: How more available memory can improve productivityPrincipled Technologies
 
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationSafe Software
 

Recently uploaded (20)

08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men
 
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfThe Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
 
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
 
Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024
 
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024
 
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men08448380779 Call Girls In Greater Kailash - I Women Seeking Men
08448380779 Call Girls In Greater Kailash - I Women Seeking Men
 
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
 
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day Presentation
 
presentation ICT roal in 21st century education
presentation ICT roal in 21st century educationpresentation ICT roal in 21st century education
presentation ICT roal in 21st century education
 
Artificial Intelligence: Facts and Myths
Artificial Intelligence: Facts and MythsArtificial Intelligence: Facts and Myths
Artificial Intelligence: Facts and Myths
 
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptxEIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
EIS-Webinar-Prompt-Knowledge-Eng-2024-04-08.pptx
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected Worker
 
Partners Life - Insurer Innovation Award 2024
Partners Life - Insurer Innovation Award 2024Partners Life - Insurer Innovation Award 2024
Partners Life - Insurer Innovation Award 2024
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...
 
Boost PC performance: How more available memory can improve productivity
Boost PC performance: How more available memory can improve productivityBoost PC performance: How more available memory can improve productivity
Boost PC performance: How more available memory can improve productivity
 
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
 

Testing Has Many Purposes

  • 1. Testing.has_many :purposes by Alex Sharp @ajsharp
  • 3. I work for a healthcare startup called OptimisCorp.
  • 4. We’re based in Pacific Palisades, just north of Santa Monica.
  • 6.
  • 7. So if you want to work by the beach in Santa Monica, get in touch with me ;)
  • 8. Brass Tacks This is a talk about the many purposes and benefits of testing.
  • 9. Brass Tacks We’ll span the testing horizon, but...
  • 10. Brass Tacks We’re going to talk a lot about LEGACY CODE
  • 11. 4 Main Purposes of Testing 1. Assertion 2. Design 3. Communication 4. Discovery
  • 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
  • 18. Tests are commonly used as a design tool
  • 19. Or as we call it...
  • 25. TDD BDD is more clear about how and what to test.
  • 26. Contrived Example class TDD def when_to_test "first" end def how_to_test "??" end end class BDD < TDD def how_to_test "outside-in" end end
  • 27. Why test-first? I write better code when it’s test-driven
  • 28. Better like how? Simpler
  • 29. Better like how? More modular
  • 30. Better like how? More maintainable
  • 31. Better like how? More stable
  • 32. Better like how? More readable
  • 33. Better like how? MORE BETTER
  • 37. Design We can use tests to design and describe the behavior of software.
  • 38. Design We can use tests to design and describe the behavior of software. But not necessarily HOW...
  • 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
  • 42. BDD Not in terms of tests, but in terms of behavior
  • 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.
  • 54. Communication Reading code is hard
  • 56. Communication Tests can be a really useful communication tool
  • 57. Communication Cucumber greatly facilitates communication between customer and programmer
  • 58. Communication But programmers need to communicate too...
  • 60. Communication Tests can help mitigate the WTF factor and communicate the author’s intent
  • 61.
  • 62. def display_address [address1, address2].reject { |address| address.nil? }.join(", ") end
  • 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
  • 64. Communication Tests usually communicate business requirements much better than production code
  • 65. Communication We can facilitate programmer communication by using good practices in test code
  • 66. Communication So we need some “good testing practices”
  • 67. To that end... Well-named tests are important in test code as well as production code...
  • 68.
  • 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
  • 73. Good Testing Conventions It’s easy to be lazy with naming , but good naming is about communication.
  • 74. Good Testing Conventions Test methods should be short, concise and targeted
  • 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
  • 81. Communication Recap In test code aim for short, concise and clean methods
  • 82. Communication Recap One assertion per test method (if possible)
  • 83. Communication Recap Single Responsibility Principle
  • 84. Writing good tests makes it much easier to refactor
  • 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
  • 88. Prepare yourself; that was the last of the fun stuff.
  • 89. Software is more than just greenfield projects
  • 90. Legacy code is part of life.
  • 91. So let’s talk about how to test it.
  • 92. But’s first let’s clarify what we mean by “legacy code”
  • 93. For purposes of this talk... Legacy code is code without tests. - Michael Feathers, Working Effectively with Legacy Code
  • 95. Generally, we need to do one of two things when working with legacy code: 1. Refactor 2. Alter behavior
  • 96. Characterization Testing We use characterization tests when we need to make changes to untested legacy code
  • 97. Characterization Testing We need to discover what the code is doing before we can responsibly change it.
  • 98. Characterization Testing Let’s start with a real world refactoring example...
  • 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
  • 105. Characterization Testing This way we can rely on an automated testing workflow to for feedback
  • 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
  • 117. It’s not perfect, but refactoring must be done in small steps
  • 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.
  • 120. Legacy Code 1. Lack of sane design principles 2. Dependency hell 3. Unknown calling code
  • 121. Legacy Code “...in legacy code, often all bets are off.” - Michael Feathers, Working Effectively with Legacy Code
  • 122. Legacy Code How do we protect against dependencies we don’t know about?
  • 123. Ideal Scenario In the ideal scenario, you know all the places from which a particular API is being invoked
  • 124. Ideal Scenario Resolving calling code dependencies is trivial
  • 125. More Likely Scenario However, this is not likely, especially for larger apps
  • 126. More Likely Scenario Unfortunately, much of the discussion around this issue assumes awareness of calling code
  • 127. More Likely Scenario This is an unfortunate assumption
  • 128. More Likely Scenario Frequent Offender: Rails controllers
  • 129. Example Solution: Direct known calling code elsewhere
  • 130. Example Treat your app like a public API
  • 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

Editor's Notes

  1. This talk is about writing tests. First and foremost, testing code is about ensuring behavior. However, I&amp;#x2019;m interested in the less obvious benefits and purposes of testing. And more importantly, I&amp;#x2019;m interested in how we can apply that to make our lives as developers easier.
  2. In &amp;#x201C;Working with Legacy Code&amp;#x201D;, Michael Feathers talks about &amp;#x201C;exercising&amp;#x201D; code in a test harness.
  3. Michael Feather&amp;#x2019;s refers to what we call test frameworks (xUnit, rspec) as a &amp;#x201C;test harness&amp;#x201D;. In this case, rspec.
  4. Michael Feather&amp;#x2019;s refers to what we call test frameworks (xUnit, rspec) as a &amp;#x201C;test harness&amp;#x201D;. In this case, rspec.
  5. Michael Feather&amp;#x2019;s refers to what we call test frameworks (xUnit, rspec) as a &amp;#x201C;test harness&amp;#x201D;. In this case, rspec.
  6. 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.
  7. 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.
  8. Single Responsibility
  9. Easier to refactor
  10. Fewer bugs in production
  11. i.e. In other words, when testing, you should focus on describing what your code should do, not how it should do it.
  12. i.e. In other words, when testing, you should focus on describing what your code should do, not how it should do it.
  13. i.e. In other words, when testing, you should focus on describing what your code should do, not how it should do it.
  14. Among many other things, BDD is a different way of thinking about testing.
  15. 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.
  16. 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.
  17. 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.
  18. If you&amp;#x2019;re confused whether I&amp;#x2019;m talking about agile development or BDD, the answer is both, because the principles are almost identical.
  19. In other words, we waste less time writing code that we&amp;#x2019;ll never actually need
  20. This leads us nicely into tests as a communication tool. Obviously, cucumber features can be a very effective communication tools because they&amp;#x2019;re written is plain english, and so they very clearly communicate both the programmer&amp;#x2019;s and the customer&amp;#x2019;s intent.
  21. Cucumber is obviously a great example of this between customer and programmer
  22. I find that the author is often me.
  23. The display_address method is not quite so clear what it is trying to accomplish. Tests can help. After reading the tests, it&amp;#x2019;s pretty clear what the display_address method is doing.
  24. The display_address method is not quite so clear what it is trying to accomplish. Tests can help. After reading the tests, it&amp;#x2019;s pretty clear what the display_address method is doing.
  25. Especially cucumber.
  26. The description tells me exactly what is going on when a new user gets saved.
  27. The description tells me exactly what is going on when a new user gets saved.
  28. Focus is lost when we have to pour through poorly written, named and organized tests, especially if the production code is not particularly pretty.
  29. Ideally, each test method should target the assertion of one thing.
  30. Ideally, each test method should target the assertion of one thing.
  31. Ideally, each test method should target the assertion of one thing.
  32. Ideally, each test method should target the assertion of one thing.
  33. Ideally, each test method should target the assertion of one thing.
  34. So to recap, for test code to be an effective communication tool
  35. 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.
  36. Notice that there is no mention of isolation.
  37. I point this out because the term &amp;#x201C;refactor&amp;#x201D; is thrown about loosely, but for purposes of this talk, this definition is important.
  38. This thing was not fun to test.
  39. This thing was not fun to test.
  40. Rick Bradley described characterization testing in this manner as putting up scaffolds around your code.
  41. Rick Bradley described characterization testing in this manner as putting up scaffolds around your code.
  42. These are the situations where code reading becomes a very necessary part of the process.
  43. This is a refactored version of a few slides back. This is much more manageable, and we increased it&amp;#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&amp;#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.
  44. 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.
  45. Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.
  46. Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.
  47. Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.
  48. Chances are that there are no controller tests telling us anything about this endpoint or how it is being used.