• Like
  • Save
Testing Has Many Purposes
Upcoming SlideShare
Loading in...5
×
 

Testing Has Many Purposes

on

  • 1,218 views

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

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

Statistics

Views

Total Views
1,218
Views on SlideShare
1,214
Embed Views
4

Actions

Likes
0
Downloads
16
Comments
0

3 Embeds 4

http://www.slideshare.net 2
http://www.linkedin.com 1
https://www.linkedin.com 1

Accessibility

Categories

Upload Details

Uploaded via as Apple Keynote

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment
  • This talk is about writing tests. First and foremost, testing code is about ensuring behavior. However, I’m interested in the less obvious benefits and purposes of testing. And more importantly, I’m interested in how we can apply that to make our lives as developers easier.
  • In “Working with Legacy Code”, Michael Feathers talks about “exercising” code in a test harness.
  • Michael Feather’s refers to what we call test frameworks (xUnit, rspec) as a “test harness”. In this case, rspec.
  • Michael Feather’s refers to what we call test frameworks (xUnit, rspec) as a “test harness”. In this case, rspec.
  • Michael Feather’s refers to what we call test frameworks (xUnit, rspec) as a “test harness”. 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’re confused whether I’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’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’re written is plain english, and so they very clearly communicate both the programmer’s and the customer’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’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’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 “refactor” 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’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’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.

Testing Has Many Purposes Testing Has Many Purposes Presentation Transcript

  • 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