shoulda request params macro

So in a flash of inspiration on the airplane last week, I jotted down a new version of my shoulda params pattern. Put this pastie in test/shoulda_macros/request_params.rb

The general idea is this:


a_get_to(:index) do
  with_params( {:foo => 'bar'}, "valid params" ) do
    should_respond_with :success

    with_params( :more => 'nested stuff' ) do
      should "do something extra special" do
         assert true
      end
    end

  end
end


Here’s what it looks like in action, taken from the ScrumNinja.com code:

require File.dirname(__FILE__) + '/../test_helper'

class MainControllerTest < ActionController::TestCase
  @@message = "you guys rock!" 

  context "When not logged in" do
    setup do
      login_as( nil )
    end
    %w[ index tour contact api ].each do |action|
      a_get_to( action, params=false ) do
        should_respond_with :success
      end
    end

    a_get_to( :feedback, params=false ) do
      should_redirect_to "'/login'" 
    end    

    a_post_to( :contact, :message => @@message ) do
      should_send_email :to => "Mailer::GO_EMAIL", :body => %r{#@@message}
      should_redirect_to "'/'" 
    end
  end

  context "When logged in" do
    setup do
      login_as( @user = create_user )
    end
    a_post_to( :feedback, :message => @@message ) do
      should_send_email :to => "Mailer::GO_EMAIL", :body => %r{#@@message}
      should_respond_with :redirect
    end
  end

end


I previously posted about global params hashes for nested shoulda contexts in functional tests. This post is a followup that shows how my solution has since crystallized.

Here’s the special sauce to add to your test_helper.rb:

ActionController::TestCase.class_eval do
  # special overload methods for "global"/nested params
  [ :get, :post ].each do |overloaded_method|
    define_method overloaded_method do |*args|
      action,params,extras = *args
      super action, @params.merge( params || {} ), *extras
    end
  end

  def setup
    super
    @params = {}
  end
end

This creates a @params variable that gets merged with all HTTP requests. Now you can use global params in your nested contexts as such:

class FooControllerTest < ActionController::TestCase
  def setup
    super
    @params[:security_token] = 'abc123' # add any global params you need here
    @event = create_event    
  end

  context "A POST to :action" do
    setup do
      @action = lambda{ post :action, :id => @event.id }
    end

    %w[ attending not_attending maybe_attending ].each do |status|
      context "with :status = '#{status}'" do
        setup do
          @params[:status] = status
          @action.call
        end          
        should_respond_with :success
        should_change "Rsvp.count", :by => 1
        should "create the proper Rsvp object" do
          assert Rsvp.find_by_user_id_and_event_id_and_status( @user.id, @event.id, status )
        end
      end
    end
  end

end

I just cooked up a macro where I needed to access an instance variable from the setup blocks. However the scope of should statements are at the class level, so we don’t have access to instance variables from there (only class variables). I found the solution inside the code for should_redirect_to, which evals the object in the binding of the should block.


class ProjectsControllerTest < ActionController::TestCase
  def setup
    super
    @project = create_project
  end

  %w[ client developer ].each do |role|
    context "A #{role} user" do
      setup do
        login_as( user = create_user )
        @project.create_role_for!( user, role )
      end

      should_not_allow :destroy, "@project" 
      should_not_allow :edit, "@project"    
    end
  end
end
This next file goes in test/shoulda_macros/*.rb, which will be auto-loaded:

Test::Unit::TestCase.class_eval do

  def self.should_not_allow action, object, msg=nil
    msg ||= "a #{object.class.to_s.downcase}" 
    should "not be able to #{action} #{msg}" do
      object = eval(object, self.send(:binding), __FILE__, __LINE__)
      get action, :id => object.id
      assert_response 404
    end
  end

  def self.should_allow action, object, msg=nil
    msg ||= "a #{object.class.to_s.downcase}" 
    should "be able to #{action} #{msg}" do
      object = eval(object, self.send(:binding), __FILE__, __LINE__)
      get action, :id => object.id
      assert_response :success
    end
  end

end

I recently developed this pattern in order to clean up my tests and shoulda contexts. The initial inspiration was that I needed a global parameter to be passed all the time, for security purposes. Then I figured I could also use it with my nested contexts, to get cleaner tests. The special sauce is the overloading of get and post methods, which merges with the @params instance variable. I am also using the @action variable to hold a proc, so that I can keep my tests DRYer. Please let me know what you think in the comments!

PS also notice the use of the slick new should_change macro, which assists me in keeping to one assert per should (and not having to put assert_difference blocks in my setups). (thanks to Ryan McGeary! lighthouse ticket, github commit )


require File.dirname(__FILE__) + '/../test_helper'
require 'events_controller'
require "test/unit" 

# Re-raise errors caught by the controller.
class EventsController; def rescue_action(e) raise e end; end

class EventsControllerTest < Test::Unit::TestCase

  ## special sauce for global parameters
  [ :get, :post ].each do |overloaded_method|
    define_method overloaded_method do |*args|
      action,params,extras = *args
      super action, @params.merge( params || {} ), *extras
    end
  end

  def setup
    @controller           = EventsController.new
    @request              = ActionController::TestRequest.new
    @response             = ActionController::TestResponse.new
    @event = create_event
    @user = User.first || create_user
    @params = {:security_token => 'abc123' } # add any global params you need here
  end

  context "A POST to :rsvp" do
    setup do
      @action = lambda{ post :rsvp, :id => @event.id }
    end

    context "with no RSVP existing in DB" do
      setup do
        if rsvp = @user.rsvps.find_by_event_id( @event.id )
          rsvp.destroy
        end
      end

      %w[ attending not_attending maybe_attending ].each do |status|
        context "with :status = '#{status}'" do
          setup do
            @params[:status] = status
            @action.call
          end          
          should_respond_with :success
          should_change "Rsvp.count", :by => 1
          should "create the proper Rsvp object" do
            assert Rsvp.find_by_user_id_and_event_id_and_status( @user.id, @event.id, status )
          end          
        end
      end
    end

    context "with an existing RSVP in DB" do
      setup do
        @rsvp = create_rsvp( :event_id => @event.id, :user_id => @user.id, :status => 'not_attending' )
      end

      context "" do    # HACK: this blank context is needed to separate the create_rsvp from the should_not_change call (without it, the count would change by 1)
        setup do
          @params[:status] = 'attending'
          @action.call
        end
        should_respond_with :success
        should_not_change "Rsvp.count" 
        should "update the status" do
          assert_equal @params[:status], @rsvp.reload.status
        end
      end      
    end

  end

  context "a POST to :create" do
    setup do
      @action = lambda{ post :create }
    end

    context "when the form fields are blank" do
      setup do
        @params[:event] = {}
      end
      should_respond_with :success
      should_render_template :edit

      should "show validation errors" do
        assert_select '.fieldWithErrors'
      end
    end

    context "with valid params and :publish => true" do
      setup do
        @params[:event] = { :title => "Some New Event", :starts_on => Date.today, :all_day => true, :ends_on => Date.today+1 }
        @params[:publish] = true
        @action.call
      end
      should "show up in event index" do
        @event = Event.find_by_title("Some New Event")
        assert Event.visible.include?( @event )
      end

      context "and no end date" do
        setup do
          @params[:ends_on] = nil
          @action.call
        end

        should_respond_with :success
        should_render_template :show

        should "show up in event index" do          
          @event = Event.find_by_title("Some New Event")
          assert Event.visible.include?( @event )
        end
      end

    end
  end
end

staying DRY with custom shoulds

Recently I’ve been refactoring legacy Test::Unit code to use shoulda. Sometimes you can’t factor out repetitive stuff into a context, but you still want to be DRY. Below is an example case of refactoring into a custom should. Note that you need to use class variables, and that the self.should_* method needs to be defined before it’s actually used, since shoulda is built upon metaprogramming which gets evaluated at the class level.


...

  def self.should_request sym
    sym = sym.to_s
    context "when requesting #{sym}" do
      setup do
        get :download, {:id => @@podcast.id, :type => sym}, @@session
      end
      should "ask for a device" do
         assert_match /Please select a device to download content/,  @response.body
      end
      should "increment the message count when sent #add_to_device" do
        assert_difference 'Message.count( :conditions => "device_id = 1")', @@podcast.episodes.size do
          xhr :post, :add_to_device, {:id => 1, :type => 'all', :podcast_id => @@podcast.id}, @@session
        end
      end
    end
  end

  context "A valid account" do
    setup do
      @account = accounts(:accounts_002)
      @@session = {:account_id => @account.id}
      @account.devices << Device.find(1)
      @@podcast = podcasts(:joe_cartoon)
      @@session.merge!({:podcast_episodes => @@podcast.episodes})
    end

    should_request :all
    should_request :latest
    should_request :episode
  end

...

Shoulda talk from SF Ruby Meetup

We hosted the SF Ruby Meetup last night, which was a smashing success. It was fun to have 50+ Ruby geeks crammed into our office. :)

Here are the slides from my presentation about Shoulda.

textmate bundle for shoulda

I’m pleased to announce a textmate bundle that I assembled for the wonderfully simple shoulda testing framework.

The snippets were contributed by Dan Croak, James Golick, and Sam Livingston-Gray, and mashed up by me.

Grab the latest version here: http://macromates.com/svn/Bundles/trunk/Review/Bundles/Ruby%20Shoulda.tmbundle

a yaml_to_shoulda rake task

Inspired by err’s cryptic yaml_to_spec rake task, I wrote my own version for shoulda, based off of Jeremy Hubert’s textmate bundle It’s a bit less cryptic, and it for extra nerd points it uses recursion to allow for nested contexts. :)


desc "Converts a YAML file into a Shoulda skeleton" 
task :yaml_to_shoulda do
  require 'yaml'

  def yaml_to_context hash, indent=0
    indent1 = '  '*indent
    indent2 = '  '*(indent+1)
    hash.each_pair do |context,shoulds|
      puts indent1+"context \"#{context}\" do" 
      puts    
      shoulds.each do |should|
        yaml_to_context( should, indent+1 ) and next if should.is_a?( Hash )
        puts indent2+"should_eventually \"#{should.gsub(/^should +/,'')}\" do" 
        puts indent2+"end" 
        puts
      end
    puts indent1+"end" 
  end 

  yaml_to_context( YAML.load_file( ENV['FILE'] || !puts("Pass in FILE argument.") && exit ) )
end

Here is an example YAML file and it’s output:

This blog post:
- should mention shoulda
- should be concise
- should be written by me
- when saved as a draft:
  - should have multiple revisions
  - should not be published publicly



context "This blog post" do

  should_eventually "mention shoulda" do
  end

  should_eventually "be concise" do
  end

  should_eventually "be written by me" do
  end

  context "when saved as a draft" do

    should_eventually "have multiple revisions" do
    end

    should_eventually "not be published publicly" do
    end

  end
end

I will be adding this to my shoulda textmate bundle.

shoulda: a spec framework without the hype

This morning I just discovered Shoulda, a really sweet testing framework that gives context and behavior without the whizz-bang syntax of RSpec and test-spec. Check it out!

My only complaints so far:

  • the example code on their homepage is missing “do” keywords after some of the “should” statements. oops!
  • I can no longer run an individual test in textmate with APPLE-shift-R, as there is no literal method to run, just a block. Hopefully this will be fixed with a forthcoming shoulda textmate bundle.
actionmailer ActionView ActiveRecord activesupport agile ajax alphadecimal audio autotest BDD blocks capistrano ssh ruby console controller css dashboard widget delegate dog puppy naming name DRM email obfuscation exceptions factories factories-and-workers factory pattern filemerge find and replace finder fink fixtures fun functional testing gem git google maps haml helper helpers imagemagick Intertrust irb javascript jquery jrails logo macro math meetup model openssl OS X patch Pioneer Electronics plugin polymorphism project management prototype.js rails rails gotcha rails,patch,validations railsconf rake rmagick RSA encryption ruby ruby on rails script scrum scrum lessons rails scrumninja shoulda subversion SyncTV TDD testing textmate tips tricks unique hashes unix shell validation view yaml zebra stripes