diff --git a/README.rdoc b/README.rdoc index 7c9f1e1e..761bd170 100644 --- a/README.rdoc +++ b/README.rdoc @@ -90,6 +90,19 @@ Macros to test the most common controller patterns... end end +Test entire controllers in a few lines... + + class PostsControllerTest < Test::Unit::TestCase + should_be_restful do |resource| + resource.parent = :user + + resource.create.params = { :title => "first post", :body => 'blah blah blah'} + resource.update.params = { :title => "changed" } + end + end + +should_be_restful generates 40 tests on the fly, for both html and xml requests. + === Helpful Assertions (ThoughtBot::Shoulda::Assertions) More to come here, but have fun with what's there. diff --git a/lib/shoulda/controller/macros.rb b/lib/shoulda/controller/macros.rb index 1dd77155..2df5121d 100644 --- a/lib/shoulda/controller/macros.rb +++ b/lib/shoulda/controller/macros.rb @@ -22,7 +22,53 @@ module ThoughtBot # :nodoc: # end # # Would produce 5 tests for the +show+ action + # + # Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your + # controller responds restfully to a variety of requested formats. module Macros + # :section: should_be_restful + # Generates a full suite of tests for a restful controller. + # + # The following definition will generate tests for the +index+, +show+, +new+, + # +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats: + # + # should_be_restful do |resource| + # resource.parent = :user + # + # resource.create.params = { :title => "first post", :body => 'blah blah blah'} + # resource.update.params = { :title => "changed" } + # end + # + # This generates about 40 tests, all of the format: + # "on GET to :show should assign @user." + # "on GET to :show should not set the flash." + # "on GET to :show should render 'show' template." + # "on GET to :show should respond with success." + # "on GET to :show as xml should assign @user." + # "on GET to :show as xml should have ContentType set to 'application/xml'." + # "on GET to :show as xml should respond with success." + # "on GET to :show as xml should return as the root element." + # The +resource+ parameter passed into the block is a ResourceOptions object, and + # is used to configure the tests for the details of your resources. + # + def should_be_restful(&blk) # :yields: resource + resource = ResourceOptions.new + blk.call(resource) + resource.normalize!(self) + + resource.formats.each do |format| + resource.actions.each do |action| + if self.respond_to? :"make_#{action}_#{format}_tests" + self.send(:"make_#{action}_#{format}_tests", resource) + else + should "test #{action} #{format}" do + flunk "Test for #{action} as #{format} not implemented" + end + end + end + end + end + # :section: Test macros # Macro that creates a test asserting that the flash contains the given value. # val can be a String, a Regex, or nil (indicating that the flash should not be set) diff --git a/lib/shoulda/controller/resource_options.rb b/lib/shoulda/controller/resource_options.rb index ee935a33..e094f3a8 100644 --- a/lib/shoulda/controller/resource_options.rb +++ b/lib/shoulda/controller/resource_options.rb @@ -1,10 +1,234 @@ module ThoughtBot # :nodoc: module Shoulda # :nodoc: module Controller + # Formats tested by #should_be_restful. Defaults to [:html, :xml] VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc: VALID_FORMATS.each {|f| require "shoulda/controller/formats/#{f}"} + # Actions tested by #should_be_restful VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc: + + # A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller. + # + # Example: + # class UsersControllerTest < Test::Unit::TestCase + # fixtures :all + # + # def setup + # ...normal setup code... + # @user = User.find(:first) + # end + # + # should_be_restful do |resource| + # resource.identifier = :id + # resource.klass = User + # resource.object = :user + # resource.parent = [] + # resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy] + # resource.formats = [:html, :xml] + # + # resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13} + # resource.update.params = { :name => "sue" } + # + # resource.create.redirect = "user_url(@user)" + # resource.update.redirect = "user_url(@user)" + # resource.destroy.redirect = "users_url" + # + # resource.create.flash = /created/i + # resource.update.flash = /updated/i + # resource.destroy.flash = /removed/i + # end + # end + # + # Whenever possible, the resource attributes will be set to sensible defaults. + # + class ResourceOptions + # Configuration options for the create, update, destroy actions under should_be_restful + class ActionOptions + # String evaled to get the target of the redirection. + # All of the instance variables set by the controller will be available to the + # evaled code. + # + # Example: + # resource.create.redirect = "user_url(@user.company, @user)" + # + # Defaults to a generated url based on the name of the controller, the action, and the resource.parents list. + attr_accessor :redirect + + # String or Regexp describing a value expected in the flash. Will match against any flash key. + # + # Defaults: + # destroy:: /removed/ + # create:: /created/ + # update:: /updated/ + attr_accessor :flash + + # Hash describing the params that should be sent in with this action. + attr_accessor :params + end + + # Configuration options for the denied actions under should_be_restful + # + # Example: + # context "The public" do + # setup do + # @request.session[:logged_in] = false + # end + # + # should_be_restful do |resource| + # resource.parent = :user + # + # resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy] + # resource.denied.flash = /get outta here/i + # resource.denied.redirect = 'new_session_url' + # end + # end + # + class DeniedOptions + # String evaled to get the target of the redirection. + # All of the instance variables set by the controller will be available to the + # evaled code. + # + # Example: + # resource.create.redirect = "user_url(@user.company, @user)" + attr_accessor :redirect + + # String or Regexp describing a value expected in the flash. Will match against any flash key. + # + # Example: + # resource.create.flash = /created/ + attr_accessor :flash + + # Actions that should be denied (only used by resource.denied). Note that these actions will + # only be tested if they are also listed in +resource.actions+ + # The special value of :all will deny all of the REST actions. + attr_accessor :actions + end + + # Name of key in params that references the primary key. + # Will almost always be :id (default), unless you are using a plugin or have patched rails. + attr_accessor :identifier + + # Name of the ActiveRecord class this resource is responsible for. Automatically determined from + # test class if not explicitly set. UserTest => "User" + attr_accessor :klass + + # Name of the instantiated ActiveRecord object that should be used by some of the tests. + # Defaults to the underscored name of the AR class. CompanyManager => :company_manager + attr_accessor :object + + # Name of the parent AR objects. Can be set as parent= or parents=, and can take either + # the name of the parent resource (if there's only one), or an array of names (if there's + # more than one). + # + # Example: + # # in the routes... + # map.resources :companies do + # map.resources :people do + # map.resources :limbs + # end + # end + # + # # in the tests... + # class PeopleControllerTest < Test::Unit::TestCase + # should_be_restful do |resource| + # resource.parent = :companies + # end + # end + # + # class LimbsControllerTest < Test::Unit::TestCase + # should_be_restful do |resource| + # resource.parents = [:companies, :people] + # end + # end + attr_accessor :parent + alias parents parent + alias parents= parent= + + # Actions that should be tested. Must be a subset of VALID_ACTIONS (default). + # Tests for each actionw will only be generated if the action is listed here. + # The special value of :all will test all of the REST actions. + # + # Example (for a read-only controller): + # resource.actions = [:show, :index] + attr_accessor :actions + + # Formats that should be tested. Must be a subset of VALID_FORMATS (default). + # Each action will be tested against the formats listed here. The special value + # of :all will test all of the supported formats. + # + # Example: + # resource.actions = [:html, :xml] + attr_accessor :formats + + # ActionOptions object specifying options for the create action. + attr_accessor :create + + # ActionOptions object specifying options for the update action. + attr_accessor :update + + # ActionOptions object specifying options for the desrtoy action. + attr_accessor :destroy + + # DeniedOptions object specifying which actions should return deny a request, and what should happen in that case. + attr_accessor :denied + + def initialize # :nodoc: + @create = ActionOptions.new + @update = ActionOptions.new + @destroy = ActionOptions.new + @denied = DeniedOptions.new + + @create.flash ||= /created/i + @update.flash ||= /updated/i + @destroy.flash ||= /removed/i + @denied.flash ||= /denied/i + + @create.params ||= {} + @update.params ||= {} + + @actions = VALID_ACTIONS + @formats = VALID_FORMATS + @denied.actions = [] + end + + def normalize!(target) # :nodoc: + @denied.actions = VALID_ACTIONS if @denied.actions == :all + @actions = VALID_ACTIONS if @actions == :all + @formats = VALID_FORMATS if @formats == :all + + @denied.actions = @denied.actions.map(&:to_sym) + @actions = @actions.map(&:to_sym) + @formats = @formats.map(&:to_sym) + + ensure_valid_members(@actions, VALID_ACTIONS, 'actions') + ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions') + ensure_valid_members(@formats, VALID_FORMATS, 'formats') + + @identifier ||= :id + @klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize + @object ||= @klass.name.tableize.singularize + @parent ||= [] + @parent = [@parent] unless @parent.is_a? Array + + collection_helper = [@parent, @object.to_s.pluralize, 'url'].flatten.join('_') + collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ') + @destroy.redirect ||= "#{collection_helper}(#{collection_args})" + + member_helper = [@parent, @object, 'url'].flatten.join('_') + member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ') + @create.redirect ||= "#{member_helper}(#{member_args})" + @update.redirect ||= "#{member_helper}(#{member_args})" + @denied.redirect ||= "new_session_url" + end + + private + + def ensure_valid_members(ary, valid_members, name) # :nodoc: + invalid = ary - valid_members + raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty? + end + end end end end diff --git a/shoulda.gemspec b/shoulda.gemspec index 779f1696..4754ced4 100644 --- a/shoulda.gemspec +++ b/shoulda.gemspec @@ -1,10 +1,10 @@ Gem::Specification.new do |s| s.name = %q{shoulda} - s.version = "2.0.1" + s.version = "2.0.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Tammer Saleh"] - s.date = %q{2008-09-20} + s.date = %q{2008-09-14} s.default_executable = %q{convert_to_should_syntax} s.email = %q{tsaleh@thoughtbot.com} s.executables = ["convert_to_should_syntax"] diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index 1cf0f282..9e34b031 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -33,11 +33,32 @@ class PostsControllerTest < Test::Unit::TestCase should_route :get, '/users/5/posts/new', :action => :new, :user_id => 5 should_route :put, '/users/5/posts/1', :action => :update, :id => 1, :user_id => 5 + context "The public" do + setup do + @request.session[:logged_in] = false + end + + should_be_restful do |resource| + resource.parent = :user + + resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy] + resource.denied.flash = /what/i + resource.denied.redirect = '"/"' + end + end + context "Logged in" do setup do @request.session[:logged_in] = true end + should_be_restful do |resource| + resource.parent = :user + + resource.create.params = { :title => "first post", :body => 'blah blah blah'} + resource.update.params = { :title => "changed" } + end + context "viewing posts for a user" do setup do get :index, :user_id => users(:first) diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index 2cd9b01c..b34bd284 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -14,30 +14,23 @@ class UsersControllerTest < Test::Unit::TestCase @user = User.find(:first) end - context "on GET to #index" do - setup { get :index } + should_be_restful do |resource| + resource.identifier = :id + resource.klass = User + resource.object = :user + resource.parent = [] + resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy] + resource.formats = [:html, :xml] - should_respond_with :success - should_render_with_layout 'users' - should_render_template :index - should_assign_to :users - end - - context "on GET to #index.xml" do - setup { get :index, :format => 'xml' } - - should_respond_with :success - should_respond_with_xml_for - should_assign_to :users - end - - context "on GET to #show" do - setup { get :show, :id => @user } + resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13, :ssn => "123456789"} + resource.update.params = { :name => "sue" } - should_respond_with :success - should_render_with_layout 'users' - should_render_template :show - should_assign_to :user + resource.create.redirect = "user_url(@user)" + resource.update.redirect = "user_url(@user)" + resource.destroy.redirect = "users_url" + + resource.create.flash = /created/i + resource.update.flash = /updated/i + resource.destroy.flash = /removed/i end - end