Your SlideShare is downloading. ×
0
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 (tes...
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)...
class Person
  attr_accessor :interests

  def initialize
    @interests = []
  end
end
describe Person do
  before(:each)...
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
  ...
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

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 ...
A cucumber feature
Scenario: Creating a new owner
  When I go to the new owner page
  And I fill in the following:
    | N...
A cucumber feature
Scenario: Creating a new owner
  When I go to the new owner page
  And I fill in the following:
    | N...
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...
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' ...
describe User, '#new' do
  before :each do
    @user = Factory.build(:user)
  end

  it 'should send an email when saved' ...
it "should not display the same clinic more than once" do
  clinics = @user.find_all_clinics_by_practice(@user.practice)
 ...
it "should not display the same clinic more than once" do
  clinics = @user.find_all_clinics_by_practice(@user.practice)
 ...
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..."
 ...
X
describe Post do
  it "should successfully save" do
    @post = Post.new :title => "Title", :body => "profound words..."...
describe Post do
  it "should successfully save" do
    @post = Post.new :title => "Title", :body => "profound words..."
 ...
describe Post do
  it "should successfully save" do
    @post = Post.new :title => "Title", :body => "profound words..."
 ...
describe Post do
  it "should successfully save" do
    @post = Post.new :title => "Title", :body => "profound words..."
 ...
A better solution
describe Post do
  before :each do
    @post = Post.new :title => "Title", :body => "profound words..."
...
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 co...
A quick aside...


If you feel like reading some really beautiful
communicative code, take a look at sinatra.
         git...
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 Le...
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...
Characterization Testing
 Lot’s of violations here:
 1. Fat model, skinny controller
 2. Single responsibility
 3. Not mod...
This is what I want to focus on
params[:conditions].each_pair do |key,value|
  condition_id = key.to_s[10..-1].to_i
  cond...
Characterization Testing

 I want to refactor in the following ways:
 1. Push this code into the model
 2. Extract logic i...
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  ...
Start by extracting this line
params[:conditions].each_pair do |key,value|
  condition_id = key.to_s[10..-1].to_i
  condit...
describe StringExtensions, "#extract_last_number" do

  it "should respond to #extract_last_number" do
    "test_string_12...
module StringExtensions
  def extract_last_number
    result = self.to_s.scan(/(d+)/).last
    result ? result.last.to_i :...
module StringExtensions
  def extract_last_number
    result = self.to_s.scan(/(d+)/).last
    result ? result.last.to_i :...
i.e. conditions.find(:all)

params[:conditions].each_pair do |key,value|
  condition_id = key.to_s[10..-1].to_i
  condition...
params[:conditions].each_pair do |key,value|
  condition_id = key.to_s[10..-1].to_i
  condition = conditions.detect {|x| x...
def update_conditions(conditions_param = {})
  conditions_param.each_pair do |key, value|
    condition_id = key.extract_l...
def update
    @medical_history = @patient.medical_histories.find(params[:id])
    @medical_history.taken_date = Date.toda...
We started with this, in the controller...
params[:conditions].each_pair do |key,value|
  condition_id = key.to_s[10..-1]....
We finished with this, in the model

def update_conditions(conditions_param = {})
  conditions_param.each_pair do |key, val...
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]....
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...
We want to alter
   some behavior here        Calling Code

                                index.erb
PatientsControllerV2...
class PatientsController < ApplicationController
  def find_by_name
    @patients = []
    if params[:name] then
      @pa...
class V2::PatientsController < ApplicationController
  def index
    @patients = @practice.patients.all_paginated([], [], ...
class PatientsController < ApplicationController
  def index
    logger.warn "DEPRECATION WARNING! Please use /v2/patients...
Further Resources
Testing Has Many Purposes
Testing Has Many Purposes
Testing Has Many Purposes
Testing Has Many Purposes
Upcoming SlideShare
Loading in...5
×

Testing Has Many Purposes

651

Published on

Slides from a talk I gave a LARuby on 1/14/09 about testing.

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total Views
651
On Slideshare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
Downloads
17
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide
  • 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.
  • In &amp;#x201C;Working with Legacy Code&amp;#x201D;, Michael Feathers talks about &amp;#x201C;exercising&amp;#x201D; code in a test harness.
  • 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.
  • 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.
  • 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.
  • 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&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.
  • In other words, we waste less time writing code that we&amp;#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&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.
  • 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&amp;#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&amp;#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 &amp;#x201C;refactor&amp;#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&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.
  • 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.
  • Transcript of "Testing Has Many Purposes"

    1. 1. Testing.has_many :purposes by Alex Sharp @ajsharp
    2. 2. I’m Alex Sharp.
    3. 3. I work for a healthcare startup called OptimisCorp.
    4. 4. We’re based in Pacific Palisades, just north of Santa Monica.
    5. 5. We’re hiring.
    6. 6. So if you want to work by the beach in Santa Monica, get in touch with me ;)
    7. 7. Brass Tacks This is a talk about the many purposes and benefits of testing.
    8. 8. Brass Tacks We’ll span the testing horizon, but...
    9. 9. Brass Tacks We’re going to talk a lot about LEGACY CODE
    10. 10. 4 Main Purposes of Testing 1. Assertion 2. Design 3. Communication 4. Discovery
    11. 11. Purpose #1: Assertion
    12. 12. 4 Main Purposes of Testing 1. Assertion We “exercise” production code with test code to ensure it’s behavior (test harness)
    13. 13. class Person attr_accessor :interests def initialize @interests = [] end end
    14. 14. 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
    15. 15. 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
    16. 16. Tests are commonly used as a design tool
    17. 17. Or as we call it...
    18. 18. TDD Test Driven Development
    19. 19. TDD I love TDD
    20. 20. TDD I love BDD more
    21. 21. TDD Why??
    22. 22. TDD BDD > TDD
    23. 23. TDD BDD is more clear about how and what to test.
    24. 24. 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
    25. 25. Why test-first? I write better code when it’s test-driven
    26. 26. Better like how? Simpler
    27. 27. Better like how? More modular
    28. 28. Better like how? More maintainable
    29. 29. Better like how? More stable
    30. 30. Better like how? More readable
    31. 31. Better like how? MORE BETTER
    32. 32. Purpose #2: Design
    33. 33. Design
    34. 34. Design
    35. 35. Design We can use tests to design and describe the behavior of software.
    36. 36. Design We can use tests to design and describe the behavior of software. But not necessarily HOW...
    37. 37. Design i.e. Behavior Driven Development
    38. 38. 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
    39. 39. BDD BDD is a different way of thinking about testing
    40. 40. BDD Not in terms of tests, but in terms of behavior
    41. 41. 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."
    42. 42. 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."
    43. 43. 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
    44. 44. Writing Tests vs Describing Behavior BDD keeps us grounded in business requirements
    45. 45. Writing Tests vs Describing Behavior The concept of BDD is tightly coupled to agile thinking
    46. 46. Writing Tests vs Describing Behavior Only do what’s necessary to accomplish the business requirement
    47. 47. Writing Tests vs Describing Behavior Then gather feedback and refactor
    48. 48. Writing Tests vs Describing Behavior So we write more more high-value code
    49. 49. Writing Tests vs Describing Behavior And less code gets thrown out
    50. 50. Writing Tests vs Describing Behavior The difference is subtle, but significant.
    51. 51. Purpose #3: Communication
    52. 52. Communication Reading code is hard
    53. 53. Communication Sometimes programmer intent is not so clear...
    54. 54. Communication Tests can be a really useful communication tool
    55. 55. Communication Cucumber greatly facilitates communication between customer and programmer
    56. 56. Communication But programmers need to communicate too...
    57. 57. Communication Problem: Everyone else’s code is crap...
    58. 58. Communication Tests can help mitigate the WTF factor and communicate the author’s intent
    59. 59. def display_address [address1, address2].reject { |address| address.nil? }.join(", ") end
    60. 60. 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
    61. 61. Communication Tests usually communicate business requirements much better than production code
    62. 62. Communication We can facilitate programmer communication by using good practices in test code
    63. 63. Communication So we need some “good testing practices”
    64. 64. To that end... Well-named tests are important in test code as well as production code...
    65. 65. 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
    66. 66. 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
    67. 67. 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
    68. 68. 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
    69. 69. Good Testing Conventions It’s easy to be lazy with naming , but good naming is about communication.
    70. 70. Good Testing Conventions Test methods should be short, concise and targeted
    71. 71. 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
    72. 72. 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.
    73. 73. 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
    74. 74. 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.
    75. 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 More than one object being tested Too much is being tested.
    76. 76. 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
    77. 77. Communication Recap In test code aim for short, concise and clean methods
    78. 78. Communication Recap One assertion per test method (if possible)
    79. 79. Communication Recap Single Responsibility Principle
    80. 80. Writing good tests makes it much easier to refactor
    81. 81. Good Testing Principles • Descriptive naming • Single Responsibility • Short test methods => readable • Prefer conciseness and readability over DRY
    82. 82. A quick aside... If you feel like reading some really beautiful communicative code, take a look at sinatra. github.com/sinatra/sinatra
    83. 83. WARNING!!!
    84. 84. Prepare yourself; that was the last of the fun stuff.
    85. 85. Software is more than just greenfield projects
    86. 86. Legacy code is part of life.
    87. 87. So let’s talk about how to test it.
    88. 88. But’s first let’s clarify what we mean by “legacy code”
    89. 89. For purposes of this talk... Legacy code is code without tests. - Michael Feathers, Working Effectively with Legacy Code
    90. 90. Purpose #4: Discovery
    91. 91. Generally, we need to do one of two things when working with legacy code: 1. Refactor 2. Alter behavior
    92. 92. Characterization Testing We use characterization tests when we need to make changes to untested legacy code
    93. 93. Characterization Testing We need to discover what the code is doing before we can responsibly change it.
    94. 94. Characterization Testing Let’s start with a real world refactoring example...
    95. 95. To refactor is to improve the design of code without changing it’s behavior
    96. 96. 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
    97. 97. 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
    98. 98. 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
    99. 99. Characterization Testing I want to refactor in the following ways: 1. Push this code into the model 2. Extract logic into separate methods
    100. 100. Characterization Testing We need to write some characterization tests and get this action under test
    101. 101. Characterization Testing This way we can rely on an automated testing workflow to for feedback
    102. 102. 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
    103. 103. 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
    104. 104. 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
    105. 105. 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
    106. 106. 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
    107. 107. 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
    108. 108. 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
    109. 109. 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
    110. 110. 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
    111. 111. 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
    112. 112. 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
    113. 113. It’s not perfect, but refactoring must be done in small steps
    114. 114. 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
    115. 115. Legacy Code We can identify a few common problems when working with untested legacy code.
    116. 116. Legacy Code 1. Lack of sane design principles 2. Dependency hell 3. Unknown calling code
    117. 117. Legacy Code “...in legacy code, often all bets are off.” - Michael Feathers, Working Effectively with Legacy Code
    118. 118. Legacy Code How do we protect against dependencies we don’t know about?
    119. 119. Ideal Scenario In the ideal scenario, you know all the places from which a particular API is being invoked
    120. 120. Ideal Scenario Resolving calling code dependencies is trivial
    121. 121. More Likely Scenario However, this is not likely, especially for larger apps
    122. 122. More Likely Scenario Unfortunately, much of the discussion around this issue assumes awareness of calling code
    123. 123. More Likely Scenario This is an unfortunate assumption
    124. 124. More Likely Scenario Frequent Offender: Rails controllers
    125. 125. Example Solution: Direct known calling code elsewhere
    126. 126. Example Treat your app like a public API
    127. 127. Example Deprecate it gradually
    128. 128. We want to alter some behavior here Calling Code index.erb PatientsControllerV2#index PatientsController#index Known calls show.erb phantom ajax call Unknown calls
    129. 129. We want to alter some behavior here Calling Code index.erb PatientsControllerV2#index Known calls show.erb PatientsController#index phantom ajax call Unknown calls
    130. 130. 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
    131. 131. 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
    132. 132. 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
    133. 133. Further Resources
    1. A particular slide catching your eye?

      Clipping is a handy way to collect important slides you want to go back to later.

    ×