Upgrade skinny_spec to deal with newer version of RSpec due to unfreezing.

This commit is contained in:
Eric Allen 2009-05-04 00:06:46 -04:00
parent bbf3e95b21
commit feeca283b4
17 changed files with 703 additions and 203 deletions

View file

@ -39,12 +39,6 @@ specs) to see what's new and different with Skinny Spec.
Let's look at the controller specs.
describe UsersController do
def valid_attributes(args = {})
{
# Add valid attributes for the your params[:user] here!
}.merge(args)
end
describe "GET :index" do
before(:each) do
@users = stub_index(User)
@ -68,12 +62,7 @@ Let's look at the controller specs.
# ...
First thing you should see is a method definition for
<tt>valid_attributes</tt>. This will be used later by the <tt>create</tt> and <tt>update</tt>
specs to more accurately represent how the controller works in actual practice by supplying
somewhat real data for the <tt>params</tt> coming from the HTML forms.
Next we find an example group for <tt>GET :index</tt>. That <tt>stub_index</tt> method there
First thing you should see is an example group for <tt>GET :index</tt>. That <tt>stub_index</tt> method there
does a lot of work behind the curtain. I'll leave it up to you to check the documentation for it
(and its brothers and sister methods like <tt>stub_new</tt>) but I will point out that the
methods named <tt>stub_<i>controller_method</i></tt> should only be used for stubbing and
@ -82,8 +71,16 @@ use <tt>stub_find_all</tt>, <tt>stub_find_one</tt>, and <tt>stub_initialize</tt>
for this is because the former methods actually save us a step by defining an implicit
controller method request. If you add a new method to your resource routing, you'll want to
use the helper method <tt>define_request</tt> in those example groups to define an explicit
request. You can also define a method called <tt>shared_request</tt> to "share a
<tt>define_request</tt>" across shared describe blocks, like so:
request, like so:
describe "PUT :demote" do
define_request { put :demote }
# ...
end
You can also define a method called <tt>shared_request</tt> to "share" a
<tt>define_request</tt> across nested describe blocks, like so:
describe "POST :create" do
def shared_request
@ -100,16 +97,22 @@ request. You can also define a method called <tt>shared_request</tt> to "share a
end
Note: When you're adding longer, more complicated controller specs you can still leverage
implicit and shared requests by calling <tt>do_request</tt> in your spec as in the following
implicit and explicit requests by calling <tt>do_request</tt> in your spec as in the following
example:
# Let's assume this controller is _not_ CategoriesController
# Note this controller is UsersController and _not_ CategoriesController
# and that loading the categories isn't part of the default actions
describe "GET :index" do
# and cannot use the <tt>stub_<i>controller_method</i></tt> helpers
# [which create implicit requests based on the controller method in the name]
# but uses <tt>stub_find_all</tt> instead
describe "GET :new" do
before(:each) do
@user = stub_new(User)
@categories = stub_find_all(Category)
end
# ...
it "should preload categories" do
Category.should_receive(:find).with(:all)
do_request
@ -264,4 +267,4 @@ rspec_on_rails_on_crack[http://github.com/technoweenie/rspec_on_rails_on_crack/t
Also thanks and props to Hampton Catlin and Nathan Weizenbaum for the lovely and imminently useable
Haml and make_resourceful. Also also praises and glory to David Chelimsky and the Rspec crew.
Also thanks to Don Petersen for his suggestions and fixes.
Also thanks to Don Petersen, Nicolas Mérouze, Mikkel Malmberg, and Brandan Lennox for their suggestions and fixes.

View file

@ -33,7 +33,7 @@ def button_to(name, options = {}, html_options = {})
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button_to\"><div>" +
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + content_tag("button", name, html_options) + request_token_tag + "</div></form>"
end

View file

@ -6,6 +6,8 @@ class SkinnyScaffoldGenerator < Rails::Generator::NamedBase
alias_method :controller_singular_name, :controller_file_name
alias_method :controller_table_name, :controller_plural_name
default_options :skip_migration => false
def initialize(runtime_args, runtime_options = {})
super
@ -86,6 +88,13 @@ protected
def banner
"Usage: #{$0} skinny_scaffold ModelName [field:type, field:type]"
end
def add_options!(opt)
opt.separator ''
opt.separator 'Options:'
opt.on("--skip-migration",
"Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
end
def model_name
class_name.demodulize

View file

@ -1,29 +1,6 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe <%= controller_class_name %>Controller do
def valid_attributes(args = {})
{
<% if attributes.empty? -%>
# Add valid attributes for the your params[:<%= singular_name %>] here!
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
"<%= attribute.name %>" => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
"<%= attribute.name %>" => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
"<%= attribute.name %>" => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
"<%= attribute.name %>" => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
"<%= attribute.name %>" => nil<%= index < attributes.size - 1 ? "," : "" %> # Could not determine valid attribute
<%- end -%>
<%- end -%>
<% end -%>
}.merge(args)
end
describe "GET :index" do
before(:each) do
@<%= plural_name %> = stub_index(<%= class_name %>)

View file

@ -5,11 +5,10 @@ describe "<%= File.join(controller_class_path, controller_singular_name) %>/form
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :created_at => 1.week.ago, :updated_at => nil
# :name => "Foo", :address => "815 Oceanic Drive"
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
@ -21,7 +20,7 @@ describe "<%= File.join(controller_class_path, controller_singular_name) %>/form
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %> # Could not determine valid attribute
<%- end -%>
<%- end -%>
<% end -%>
<% end -%>
})
end

View file

@ -3,11 +3,11 @@ require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/
describe "<%= File.join(controller_class_path, controller_singular_name) %>/index.html.<%= template_language %>" do
before(:each) do
@<%= plural_name %> = mock_and_assign_collection(<%= model_name %>)
template.stub_render :partial => @<%= plural_name %>
template.stub! :render
end
it "should render :partial => @<%= plural_name %>" do
template.expect_render :partial => @<%= plural_name %>
template.should_receive(:render).with(:partial => @<%= plural_name %>)
do_render
end

View file

@ -8,8 +8,7 @@ describe "<%= File.join(controller_class_path, controller_singular_name) %>/_<%=
# :name => "Foo", :created_at => 1.week.ago, :updated_at => nil
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>

View file

@ -12,7 +12,7 @@ describe <%= class_name %> do
end
after(:each) do
@<%= singular_name %>.destroy unless @<%= singular_name %>.new_record?
@<%= singular_name %>.destroy
end
# Add your model specs here, please!

View file

@ -7,8 +7,7 @@ describe "<%= File.join(controller_class_path, controller_singular_name) %>/show
<% else -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type -%>
<%- when :string, :text -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>

View file

@ -2,13 +2,24 @@ module LuckySneaks
# These methods are mostly just called internally by various other spec helper
# methods but you're welcome to use them as needed in your own specs.
module CommonSpecHelpers
# Stubs out Time.now and returns value to use when comparing it. Example:
#
# time_now = stub_time_now
# @foo.some_method_that_resets_updated_at
# @foo.updated_at.should == time_now
def stub_time_now
returning Time.now do |now|
Time.stub!(:now).and_return(now)
end
end
# Returns class for the specified name. Example:
#
# class_for("foo") # => Foo
def class_for(name)
name.to_s.constantize
rescue NameError
name.to_s.classify.constantize
name.to_s.pluralize.classify.constantize
# Let any other error rise!
end
@ -42,5 +53,31 @@ module LuckySneaks
end
end
end
# Returns class description text
def class_description_text
if self.class.respond_to?(:description_text)
# Old school
self.class.description_text
else
# New school
self.class.description
end
end
# Returns description text
def self_description_text
if respond_to?(:description_text)
# Old school
description_text
else
# New school
description
end
end
def described_type
self.class.described_type
end
end
end
end

View file

@ -27,9 +27,9 @@ module LuckySneaks
alias do_request eval_request
def try_shared_request_definition
shared_request
rescue NameError
if @implicit_request
if defined?(shared_request) == "method"
shared_request
elsif @implicit_request
try_implicit_request
else
error_message = "Could not determine request definition for 'describe' context. "
@ -64,4 +64,4 @@ module LuckySneaks
end
end
end
end
end

View file

@ -41,28 +41,23 @@ module LuckySneaks
private
def create_ar_class_expectation(name, method, argument = nil, options = {})
args = []
if [:create, :update].include?(@controller_method)
args << (argument.nil? ? valid_attributes : argument)
else
unless options.delete(:only_method)
args << argument unless argument.nil?
args << hash_including(options) unless options.empty?
end
args << options unless options.empty?
method = options.delete(:find_method) if options[:find_method]
if args.empty?
return_value = class_for(name).send(method)
class_for(name).should_receive(method).and_return(return_value)
class_for(name).should_receive(method).and_return(instance_for(name))
else
return_value = class_for(name).send(method, *args)
class_for(name).should_receive(method).with(*args).and_return(return_value)
class_for(name).should_receive(method).with(*args).and_return(instance_for(name))
end
end
def create_positive_ar_instance_expectation(name, method, *args)
instance = instance_for(name)
if args.empty?
return_value = instance.send(method)
instance.should_receive(method).and_return(true)
else
return_value = instance.send(method, *args)
instance.should_receive(method).with(*args).and_return(true)
end
end
@ -73,12 +68,14 @@ module LuckySneaks
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.find</tt>.
# Examples:
#
# it_should_find :foos # => Foo.should_receive(:find).with(:all)
# it_should_find :foos, :all # An explicit version of the above
# it_should_find :foos, :conditions => {:foo => "bar"} # => Foo.should_receive(:find).with(:all, :conditions => {"foo" => "bar"}
# it_should_find :foo # => Foo.should_recieve(:find).with(@foo.id.to_s)
# it_should_find :foo, :params => "id" # => Foo.should_receive(:find).with(params[:id].to_s)
# it_should_find :foo, 2 # => Foo.should_receive(:find).with("2")
# it_should_find :foos # => Foo.should_receive(:find).with(:all)
# it_should_find :foos, :all # An explicit version of the above
# it_should_find :foos, :conditions => {:foo => "bar"} # => Foo.should_receive(:find).with(:all, :conditions => {"foo" => "bar"}
# it_should_find :foos, "joe", :method => :find_all_by_name # Foo.should_receive(:find_all_by_name).with("joe")
# it_should_find :foo # => Foo.should_recieve(:find).with(@foo.id.to_s)
# it_should_find :foo, :params => "id" # => Foo.should_receive(:find).with(params[:id].to_s)
# it_should_find :foo, 2 # => Foo.should_receive(:find).with("2")
# it_should_find :foo, "joe", :method => :find_by_name # => Foo.should_recieve(:find_by_name).with("joe")
#
# <b>Note:</b> All params (key and value) will be strings if they come from a form element and are handled
# internally with this expectation.
@ -103,7 +100,27 @@ module LuckySneaks
:all
end
end
create_ar_class_expectation name, :find, argument, options
find_method = options.delete(:method) || :find
create_ar_class_expectation name, find_method, argument, options
eval_request
end
end
# Negative version of <tt>it_should_find</tt>. This creates an expectation that
# the class never receives <tt>find</tt> at all.
def it_should_not_find(name)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should not find #{name_message}" do
if name_string == name_string.singularize
class_for(name).should_not_receive(:find)
else
class_for(name).should_not_receive(:find).with(:all)
end
eval_request
end
end
@ -121,6 +138,15 @@ module LuckySneaks
end
end
# Negative version of <tt>it_should_initialize</tt>. This creates an expectation
# that the class never recieves <tt>new</tt> at all.
def it_should_not_initialize(name)
it "should initialize a #{name}" do
class_for(name).should_not_receive(:new)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#save</tt> on the
# named instance. Example:
#
@ -136,6 +162,15 @@ module LuckySneaks
end
end
# Negative version of <tt>it_should_update</tt>. This creates an expectation
# that the instance never receives <tt>save</tt> at all.
def it_should_not_save(name)
it "should not save the #{name}" do
instance_for(name).should_not_receive(:save)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#update_attributes</tt>
# on the named instance. Takes optional argument for <tt>params</tt> to specify in the
# expectation. Examples:
@ -148,7 +183,16 @@ module LuckySneaks
# for the inevitable re-rendering of the form template.
def it_should_update(name, options = {})
it "should update the #{name}" do
create_positive_ar_instance_expectation name, :update_attributes, params[options[:params]]
create_positive_ar_instance_expectation name, :update_attributes, params[name]
eval_request
end
end
# Negative version of <tt>it_should_update</tt>. This creates an expectation
# that the instance never receives <tt>update_attributes</tt> at all.
def it_should_not_update(name)
it "should not update the #{name}" do
instance_for(name).should_not_receive(:update_attributes)
eval_request
end
end
@ -168,6 +212,15 @@ module LuckySneaks
end
end
# Negative version of <tt>it_should_destroy</tt>. This creates an expectation
# that the instance never receives <tt>destroy</tt> at all.
def it_should_not_destroy(name)
it "should not destroy the #{name}" do
instance_for(name).should_not_receive(:destroy)
eval_request
end
end
# Creates expectation[s] that the controller method should assign the specified
# instance variables along with any specified values. Examples:
#
@ -192,16 +245,36 @@ module LuckySneaks
end
end
# Essentially shorthand for <tt>it_should_assign name => :nil</tt>. This method can take multiple
# instance variable names, creating this shorthand for each name. See the docs for
# <tt>it_should_assign</tt> for more information.
def it_should_not_assign(*names)
names.each do |name|
# Assuming name is a symbol
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_assign(*names)
names.each do |name|
it_should_find name
it_should_find name, :only_method => true
it_should_assign name
end
end
# Negative version of <tt>it_should_find_and_assign</tt>. This creates an
# expectation that the class never receives <tt>find</tt> at all and that
# no matching instance variable is ever created.
def it_should_not_find_and_assign(*names)
names.each do |name|
it_should_not_find name
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
@ -212,11 +285,21 @@ module LuckySneaks
# please use <tt>it_should_initialize_and_save</tt>.
def it_should_initialize_and_assign(*names)
names.each do |name|
it_should_initialize name
it_should_initialize name, :only_method => true
it_should_assign name
end
end
# Negative version of <tt>it_should_initialize_and_assign</tt>. This creates an
# expectation that the class never receives <tt>new</tt> at all and that
# no matching instance variable is ever created.
def it_should_not_initialize_and_assign(*names)
names.each do |name|
it_should_not_initialize name
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_save</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
@ -226,7 +309,7 @@ module LuckySneaks
# but not saved, just use <tt>it_should_initialize_and_assign</tt>.
def it_should_initialize_and_save(*names)
names.each do |name|
it_should_initialize name
it_should_initialize name, :only_method => true
it_should_save name
end
end
@ -240,7 +323,7 @@ module LuckySneaks
# instance is found but not saved, just use <tt>it_should_find_and_assign</tt>.
def it_should_find_and_update(*names)
names.each do |name|
it_should_find name
it_should_find name, :only_method => true
it_should_update name
end
end
@ -250,16 +333,16 @@ module LuckySneaks
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_destroy(*names)
names.each do |name|
it_should_find name
it_should_find name, :only_method => true
it_should_destroy name
end
end
# Creates an expectation that the specified collection (<tt>flash</tt> or <tt>session</tt>)
# contains the specified key and value. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
# Creates an expectation that the specified collection (<tt>flash</tt>, <tt>session</tt>,
# <tt>params</tt>, <tt>cookies</tt>) contains the specified key and value. To specify that
# the collection should be set to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set(collection, key, value = nil, &block)
it "should set #{collection}[:#{key}]" do
it "should set #{collection}[:#{key}]#{' with ' + value.inspect if value}" do
# Allow flash.now[:foo] to remain in the flash
flash.stub!(:sweep) if collection == :flash
eval_request
@ -270,7 +353,7 @@ module LuckySneaks
self.send(collection)[key].should == value
end
elsif block_given?
self.send(collection)[key].should == block.call
self.send(collection)[key].should == instance_eval(&block)
else
self.send(collection)[key].should_not be_nil
end
@ -282,6 +365,11 @@ module LuckySneaks
def it_should_set_flash(name, value = nil, &block)
it_should_set :flash, name, value, &block
end
# Wraps <tt>it_should_set :flash, :nil</tt>.
def it_should_not_set_flash(name)
it_should_set :flash, name, :nil
end
# Wraps <tt>it_should_set :session</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
@ -289,6 +377,33 @@ module LuckySneaks
it_should_set :session, name, value, &block
end
# Wraps <tt>it_should_set :session, :nil</tt>.
def it_should_not_set_session(name)
it_should_set :session, name, :nil
end
# Wraps <tt>it_should_set :params</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_params(name, value = nil, &block)
it_should_set :params, name, value, &block
end
# Wraps <tt>it_should_set :params, :nil</tt>.
def it_should_not_set_params(name)
it_should_set :params, name, :nil
end
# Wraps <tt>it_should_set :cookies</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_cookies(name, value = nil, &block)
it_should_set :cookies, name, value, &block
end
# Wraps <tt>it_should_set :cookies, :nil</tt>.
def it_should_not_set_cookies(name)
it_should_set :cookies, name, :nil
end
# Wraps the various <tt>it_should_render_<i>foo</i></tt> methods:
# <tt>it_should_render_template</tt>, <tt>it_should_render_partial</tt>,
# <tt>it_should_render_xml</tt>, <tt>it_should_render_json</tt>,
@ -407,6 +522,27 @@ module LuckySneaks
end
end
# Negative version of <tt>it_should_redirect_to</tt>.
def it_should_not_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should not redirect to #{(hint || route)}" do
eval_request
response.should_not redirect_to(instance_eval(&route))
end
end
# Creates an expectation that the controller method redirects back to the previous page
def it_should_redirect_to_referer
it "should redirect to the referring page" do
request.env["HTTP_REFERER"] = "http://test.host/referer"
eval_request
response.should redirect_to("http://test.host/referer")
end
end
alias it_should_redirect_to_referrer it_should_redirect_to_referer
private
def it_should_assign_instance_variable(name, value)
expectation_proc = case value

View file

@ -6,26 +6,13 @@ module LuckySneaks # :nodoc:
# file even more. You are encouraged to use these methods to setup the basic calls for your
# resources and only resort to the other methods when mocking and stubbing secondary objects
# and calls.
#
# Both <tt>stub_create</tt> and <tt>stub_update</tt> benefit from having a <tt>valid_attributes</tt>
# method defined at the top level of your example groups, ie the top-most "describe" block
# of the spec file. If you did not generate your specs with <tt>skinny_scaffold</tt> or
# <tt>skinny_resourceful</tt> generators, you can simply write a method like the following
# for yourself:
#
# def valid_attributes
# {
# "foo" => "bar",
# "baz" => "quux"
# }
# end
#
# Note this method employs strings as both the key and values to best replicate the way
# they are used in actual controllers where the params will come from a form.
module ControllerStubHelpers
# Stubs out <tt>find :all</tt> and returns a collection of <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:find_method</b>:: Method to use as finder call. Default is <tt>:find</tt>.
# <b>Note:</b> When specifying the method, the call is stubbed
# to accept any arguments. Caveat programmer.
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:size</b>:: Number of instances to return in the result. Default is 3.
@ -40,10 +27,13 @@ module LuckySneaks # :nodoc:
stub_formatted collection, format
params[:format] = format
end
if options.empty?
klass.stub!(:find).with(:all).and_return(collection)
if find_method = options[:find_method]
# Not stubbing specific arguments here
# If you need more specificity, write a custom example
klass.stub!(find_method).and_return(collection)
else
klass.stub!(:find).with(:all, options).and_return(collection)
klass.stub!(:find).with(:all).and_return(collection)
klass.stub!(:find).with(:all, hash_including(options)).and_return(collection)
end
end
end
@ -71,9 +61,11 @@ module LuckySneaks # :nodoc:
params[:format] = format
end
klass.stub!(:new).and_return(member)
if options[:params]
klass.stub!(:new).with(hash_including(options[:params])).and_return(member)
end
if options[:stub_save]
stub_ar_method member, :save, options[:return]
klass.stub!(:new).with(params[options[:params]]).and_return(member)
else
member.stub!(:new_record?).and_return(true)
member.stub!(:id).and_return(nil)
@ -89,32 +81,25 @@ module LuckySneaks # :nodoc:
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>post :create</tt>.
#
# <b>Note:</b> If <tt>stub_create<tt> is provided an optional <tt>:params</tt> hash
# or the method <tt>valid_attributes</tt> is defined within its scope,
# those params will be added to the example's <tt>params</tt> object. If <i>neither</i>
# are provided an <tt>ArgumentError</tt> will be raised.
# <b>Note:</b> If <tt>stub_create<tt> is provided an optional <tt>:params</tt> hash,
# those params will be added to the example's <tt>params</tt> object.
def stub_create(klass, options = {})
define_implicit_request :create
if options[:params].nil?
if self.respond_to?(:valid_attributes)
params[klass.name.underscore.to_sym] = valid_attributes
options[:params] = valid_attributes
else
error_message = "Params for creating #{klass} could not be determined. "
error_message << "Please define valid_attributes method in the base 'describe' block "
error_message << "or manually set params in the before block."
raise ArgumentError, error_message
end
end
class_name = klass.name.underscore
options[:params] ||= params[class_name]
stub_initialize klass, options.merge(:stub_save => true)
end
# Stubs out <tt>find</tt> and returns a single <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
# <b>:find_method</b>:: Method to use as finder call. Default is <tt>:find</tt>.
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
# <b>:current_object</b>:: If set to true, <tt>find</tt> will set <tt>params[:id]</tt>
# using the <tt>id</tt> of the <tt>mock_model</tt> instance
# and use that value as an argument when stubbing <tt>find</tt>
#
# Any additional options will be passed as arguments to <tt>find</tt>.You will want
# to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
@ -125,17 +110,55 @@ module LuckySneaks # :nodoc:
def stub_find_one(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if options[:format]
stub_formatted member, options[:format]
params[:format] = options[:format]
if format = options.delete(:format)
stub_formatted member, format
params[:format] = format
end
if options[:current_object]
if options.delete(:current_object)
params[:id] = member.id
if options[:stub_ar]
stub_ar_method member, options[:stub_ar], options[:return]
if ar_stub = options.delete(:stub_ar)
stub_ar_method member, ar_stub, options.delete(:return), options.delete(:update_params)
end
end
klass.stub!(:find).with(member.id.to_s).and_return(member)
if find_method = options.delete(:find_method)
klass.stub!(find_method).and_return(member)
else
# Stubbing string and non-string just to be safe
klass.stub!(:find).with(member.id).and_return(member)
klass.stub!(:find).with(member.id.to_s).and_return(member)
unless options.empty?
klass.stub!(:find).with(member.id, hash_including(options)).and_return(member)
klass.stub!(:find).with(member.id.to_s, hash_including(options)).and_return(member)
end
end
end
end
# <b>Note:</b> Use of this method with :child options (to mock
# association) is deprecated. Please use <tt>stub_association</tt>.
#
# Same as <tt>stub_find_one</tt> but setups the instance as the parent
# of the specified association. Example:
#
# stub_parent(Document, :child => :comments)
#
# This stubs <tt>Document.find</tt>, <tt>@document.comments</tt> (which
# will return <tt>Comment</tt> class), as well as <tt>params[:document_id]</tt>.
# This method is meant to be used in the controller for the specified child
# (<tt>CommentsController</tt> in this instance) in situations like:
#
# def index
# @document = Document.find(params[:document_id])
# @comments = @document.comments.find(:all)
# end
def stub_parent(klass, options = {})
returning stub_find_one(klass, options) do |member|
params[klass.name.foreign_key] = member.id
if offspring = options.delete(:child)
puts "stub_parent with :child option has been marked for deprecation"
puts "please use stub_association to create the mock instead"
member.stub!(offspring).and_return(class_for(offspring))
end
end
end
@ -154,10 +177,8 @@ module LuckySneaks # :nodoc:
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>put :update</tt>
# and stubs out the <tt>update_attribute</tt> method on the instance as well.
#
# <b>Note:</b> If <tt>stub_update<tt> is provided an optional <tt>:params</tt> hash
# or the method <tt>valid_attributes</tt> is defined within its scope,
# those params will be added to the example's <tt>params</tt> object. If <i>neither</i>
# are provided an <tt>ArgumentError</tt> will be raised.
# <b>Note:</b> If <tt>stub_update<tt> is provided an optional <tt>:params</tt> hash,
# those params will be added to the example's <tt>params</tt> object.
def stub_update(klass, options = {})
define_implicit_request :update
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :update_attributes)
@ -176,6 +197,20 @@ module LuckySneaks # :nodoc:
object.stub!("to_#{format}").and_return("#{object.class} formatted as #{format}")
end
# Creates a mock object representing an association proxy, stubs the appropriate
# method on the parent object and returns that association proxy.
# Accepts the following option:
#
# <b>:stub</b>:: Additional methods to stub on the mock proxy object
def stub_association(object, association, options = {})
# I know options isn't implemented anywhere
object_name = instance_variables.select{|name| instance_variable_get(name) == object}
returning mock("Association proxy for #{object_name}.#{association}") do |proxy|
stub_out proxy, options[:stub] if options[:stub]
object.stub!(association).and_return(proxy)
end
end
private
# Stubs out multiple methods. You shouldn't be calling this yourself and if you do
# you should be able to understand the code yourself, right?
@ -192,8 +227,12 @@ module LuckySneaks # :nodoc:
# Stubs out ActiveRecord::Base methods like #save, #update_attributes, etc
# that may be called on a found or instantiated mock_model instance.
def stub_ar_method(object, method, return_value)
object.stub!(method).and_return(return_value ? false : true)
def stub_ar_method(object, method, return_value, params = {})
if params.blank?
object.stub!(method).and_return(return_value ? false : true)
else
object.stub!(method).with(hash_including(params)).and_return(return_value ? false : true)
end
end
end
end

View file

@ -6,25 +6,51 @@ module LuckySneaks
# to make your model specs a little more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ModelSpecHelpers::ExampleGroupLevelMethods
# LuckySneaks::ModelSpecHelpers::ExampleGroupLevelMethods.
#
# <b>Note:</b> The validation matchers are only meant to be used for simple validation checking
# not as a one-size-fits-all solution.
# Also check out the methods in LuckySneaks::ModelSpecHelpers::AssociationMatcher
# for some helpful matcher helper methods to use with these methods if you want to spec
# options on your association setups.
module ModelSpecHelpers
include LuckySneaks::CommonSpecHelpers
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
class AssociationMatcher # :nodoc:
def initialize(associated, macro)
# These methods cannot be used alone but are used in compliment with the association
# matchers in LuckySneaks::ModelSpecHelpers like <tt>have_many</tt>. Example:
#
# describe User do
# it "should have many memberships" do
# User.should have_many(:memberships)
# end
#
# it "should have many sites through memberships" do
# User.should have_many(:sites).through(:memberships)
# end
#
# it "should belong to a manager" do
# User.should belong_to(:manager).with_counter_cache
# end
# end
#
# <b>Note:</b> To spec these sorts of options using the example block helpers like
# <tt>it_should_have_many</tt>, just add them as options directly. This will use
# <tt>with_options</tt> rather than any specific matcher helpers but will have the same
# effects. Example:
#
# describe User do
# it_should_have_many :sites, :through => :memberships
# end
class AssociationMatcher
def initialize(associated, macro) # :nodoc:
@associated = associated
@macro = macro
@options = {}
end
def matches?(main_model)
def matches?(main_model) # :nodoc:
unless main_model.respond_to?(:reflect_on_association)
if main_model.class.respond_to?(:reflect_on_association)
main_model = main_model.class
@ -39,7 +65,7 @@ module LuckySneaks
end
end
def failure_message
def failure_message # :nodoc:
if @not_model
" expected: #{@not_model} to be a subclass of ActiveRecord::Base class, but was not"
elsif @association
@ -49,7 +75,7 @@ module LuckySneaks
end
end
def negative_failure_message
def negative_failure_message # :nodoc:
if @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
@ -77,7 +103,7 @@ module LuckySneaks
self
end
def with_counter_cache(counter_cache = false)
def with_counter_cache(counter_cache = true)
if counter_cache
@options[:counter_cache] = counter_cache
end
@ -169,86 +195,208 @@ module LuckySneaks
private
def class_or_instance
@model_spec_class_or_instance ||= class_for(self.class.description_text) || instance
@model_spec_class_or_instance ||= class_for(described_type) || instance
end
def instance
@model_spec_instance ||= instance_for(self.class.description_text)
@model_spec_instance ||= instance_for(described_type)
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Most of these methods are wrappers for
# to simplify and DRY up common expectations. Some of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ModelSpecHelpers for more information.
#
# <b>Note:</b> The validation matchers are only meant to be used for simple validation checking
# not as a one-size-fits-all solution.
module ExampleGroupLevelMethods
# Creates an expectation that the current model being spec'd has a <tt>belongs_to</tt>
# association with the specified model.
# Creates an expectation that the current model being spec'd has a <tt>belong_to</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>belong_to</tt> spec like this:
#
# it_should_belong_to :document, :counter_cache => true
#
# which is the same as writing out:
#
# it "should belong to document" do
# Comment.should belong_to(:document).with_options(:counter_cache => true)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>belong_to</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_belong_to(model)
def it_should_belong_to(model, options = {})
it "should belong to a #{model}" do
class_or_instance.should belong_to(model)
if options.empty?
class_or_instance.should belong_to(model)
else
class_or_instance.should belong_to(model).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_one</tt>
# association with the specified model.
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_one</tt> spec like this:
#
# it_should_have_one :last_comment, :class_name => "Comment", :order => "created_at DESC"
#
# which is the same as writing out:
#
# it "should have one document" do
# Document.should have_one(:last_comment).with_options(:class_name => "Comment", :order => "created_at DESC")
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_one</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_one(model)
def it_should_have_one(model, options = {})
it "should have one #{model}" do
class_or_instance.should have_one(model)
if options.empty?
class_or_instance.should have_one(model)
else
class_or_instance.should have_one(model).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_many</tt>
# association with the specified model.
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_many</tt> spec like this:
#
# it_should_have_many :memberships, :through => :sites
#
# which is the same as writing out:
#
# it "should have many memberships" do
# User.should have_many(:memberships).with_options(:through => :sites)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_many</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_many(models)
def it_should_have_many(models, options = {})
it "should have many #{models}" do
class_or_instance.should have_many(models)
if options.empty?
class_or_instance.should have_many(models)
else
class_or_instance.should have_many(models).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_and_belong_to_many</tt>
# association with the specified model.
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_and_belong_to_many</tt> spec like this:
#
# it_should_have_and_belong_to_many :documents, :include => :attachments
#
# which is the same as writing out:
#
# it "should belong to document" do
# User.should have_and_belong_to_many(:documents).with_options(:include => :attachments)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_and_belong_to_many</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_and_belong_to_many(models)
def it_should_have_and_belong_to_many(models, options = {})
it "should have and belong to many #{models}" do
class_or_instance.should have_and_belong_to_many(models)
if options.empty?
class_or_instance.should have_and_belong_to_many(models)
else
class_or_instance.should have_and_belong_to_many(models).with_options(options)
end
end
end
# Creates an expectation that new instances of the model being spec'd
# should initialise the specified attributes with a default value.
#
# it_should_default_attributes :status => 'new'
#
def it_should_default_attributes(hash_attribute_values)
hash_attribute_values.each_pair do |a,v|
it "should default #{a} attribute to #{v}" do
class_or_instance.new.send(a).should == v
end
end
end
# Creates an expectation that the current model being spec'd <tt>validates_presence_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_presence_of(attribute, message = I18n.translate('activerecord.errors.messages')[:blank])
def it_should_validate_presence_of(attribute, message = default_error_message(:blank))
it "should not be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should include(message)
end
end
# Negative version of <tt>it_should_validate_presence_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_presence_of(attribute, message = default_error_message(:blank))
it "should be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_inclusion_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_inclusion_of(attribute, options = {}, message = default_error_message(:inclusion))
it "should validate #{attribute} is in #{options[:in].to_s}" do
# We specifically do not try to go below the range on String and character ranges because that problem set is unpredictable.
lower = options[:in].first.respond_to?(:-) ? options[:in].first - 0.0001 : nil
higher = options[:in].last.succ
instance.send "#{attribute}=", lower
instance.errors_on(attribute).should include(message)
instance.send "#{attribute}=", higher
instance.errors_on(attribute).should include(message)
instance.send "#{attribute}=", (lower+higher)/2
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_numericality_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_numericality_of(attribute, message = I18n.translate('activerecord.errors.messages')[:not_a_number])
def it_should_validate_numericality_of(attribute, message = default_error_message(:not_a_number))
it "should validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should include(message)
end
end
# Negative version of <tt>it_should_validate_numericality_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_numericality_of(attribute, message = default_error_message(:not_a_number))
it "should not validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_confirmation_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_confirmation_of(attribute, message = I18n.translate('activerecord.errors.messages')[:confirmation])
def it_should_validate_confirmation_of(attribute, message = default_error_message(:confirmation))
it "should validate confirmation of #{attribute}" do
dummy_value = dummy_value_for(instance, attribute) || "try a string"
instance.send "#{attribute}=", dummy_value
@ -263,20 +411,33 @@ module LuckySneaks
#
# <b>Note:</b> This method will fail completely if <tt>valid_attributes</tt>
# does not provide all the attributes needed to create a valid record.
def it_should_validate_uniqueness_of(attribute, message = I18n.translate('activerecord.errors.messages')[:taken])
it "should validate #{attribute} confirmation" do
previous_instance = class_for(self.class.description_text).create!(valid_attributes)
def it_should_validate_uniqueness_of(attribute, message = default_error_message(:taken))
it "should validate uniqueness of #{attribute}" do
previous_instance = instance.class.create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should include(message)
previous_instance.destroy
end
end
# Negative version of <tt>it_should_validate_uniqueness_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_uniqueness_of(attribute, message = default_error_message(:taken))
it "should not validate uniqueness of #{attribute}" do
previous_instance = instance.class.create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should_not include(message)
previous_instance.destroy
end
end
# Creates an expectation that the current model being spec'd accepts the specified values as
# valid for the specified attribute. This is most likely used with <tt>validates_format_of</tt>
# but there's nothing saying it couldn't be another validation.
def it_should_accept_as_valid(attribute, *values)
values.each do |value|
values.flatten.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
@ -296,7 +457,7 @@ module LuckySneaks
# spec'ing the actual error message.
def it_should_not_accept_as_valid(attribute, *values)
options = values.extract_options!
values.each do |value|
values.flatten.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
@ -312,6 +473,7 @@ module LuckySneaks
end
end
end
# Creates an expectation that the current model being spec'd doesn't allow mass-assignment
# of the specified attribute.
def it_should_not_mass_assign(attribute)
@ -321,6 +483,14 @@ module LuckySneaks
}.should_not change(instance, attribute)
end
end
def default_error_message(attribute)
if defined?(I18n)
I18n.translate attribute, :scope => "activerecord.errors.messages"
else
ActiveRecord::Errors.default_error_messages[attribute]
end
end
end
end
end
end

View file

@ -6,9 +6,10 @@ module LuckySneaks
# to make your view specs less brittle and more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ViewSpecHelpers::ExampleGroupLevelMethods
# LuckySneaks::ViewSpecHelpers::ExampleGroupLevelMethods.
module ViewSpecHelpers
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ViewStubHelpers
include ActionController::PolymorphicRoutes
def self.included(base) # :nodoc:
@ -21,7 +22,7 @@ module LuckySneaks
have_tag("form[action=#{path}]")
end
# Wraps a matcher that checks is the receiver contains any of several form elements
# Wraps a matcher that checks if the receiver contains any of several form elements
# that would return sufficient named parameters to allow editing of the specified
# attribute on the specified instance. Example:
#
@ -36,7 +37,8 @@ module LuckySneaks
# <textarea name="foo[bar]"></textarea>
def allow_editing(instance, attribute)
instance_name = instance.class.name.underscore.downcase
if instance.send(attribute).is_a?(Time)
column = instance.column_for_attribute(attribute)
if column && [Date, Time].include?(column.klass)
have_tag(
"input[name='#{instance_name}[#{attribute}]'],
select[name=?]", /#{instance_name}\[#{attribute}\(.*\)\]/
@ -48,10 +50,20 @@ module LuckySneaks
select[name='#{instance_name}[#{attribute}]'],
textarea[name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute.to_s.tableize.singularize}_ids][]']"
input[type='checkbox'][name='#{instance_name}[#{attribute.to_s.tableize.singularize}_ids][]'],
input[type='radio'][name='#{instance_name}[#{attribute}]']"
)
end
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element
# whose <tt>enctype</tt> attribute is set to <tt>"multipart/form-data"<tt>
# and contains an <tt>INPUT</tt> element whose <tt>name</tt> attribute correlates
# with the provided instance and attribute.
def allow_uploading(instance, attribute)
instance_name = instance.class.name.underscore.downcase
have_tag("form[enctype='multipart/form-data'] input[type='file'][name='#{instance_name}[#{attribute}]']")
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path or a <tt>FORM</tt>
@ -78,7 +90,14 @@ module LuckySneaks
# Wraps <tt>have_link_or_button_to polymorphic_path(instance)<tt> which
# corresponds with the <tt>show</tt> method of the controller.
def have_link_or_button_to_show(instance)
have_link_or_button_to polymorphic_path(instance)
path = polymorphic_path(instance)
have_tag(
"a[href='#{path}'],
form[action='#{path}'][method='get'] input,
form[action='#{path}'][method='get'] button,
form[action='#{path}'] input[name='_method'][value='get'] + input,
form[action='#{path}'] input[name='_method'][value='get'] + button"
)
end
alias have_link_to_show have_link_or_button_to_show
alias have_button_to_show have_link_or_button_to_show
@ -99,7 +118,8 @@ module LuckySneaks
path = polymorphic_path(instance)
have_tag(
"form[action='#{path}'] input[name='_method'][value='delete'] + input,
form[action='#{path}'] input[name='_method'][value='delete'] + button"
form[action='#{path}'] input[name='_method'][value='delete'] + button,
a[href=\"#{path}\"][onclick*=\"f.method = 'POST'\"][onclick*=\"m.setAttribute('name', '_method'); m.setAttribute('value', 'delete')\"]"
)
end
@ -121,9 +141,9 @@ module LuckySneaks
def mock_and_assign(klass, *args)
options = args.extract_options!
mocked = if options[:stub]
mock_model(klass, options[:stub])
self.respond_to?(:stub_model) ? stub_model(klass, options[:stub]) : mock_model(klass, options[:stub])
else
mock_model(klass)
self.respond_to?(:stub_model) ? stub_model(klass) : mock_model(klass)
end
yield mocked if block_given?
self.assigns[args.first || "#{klass}".underscore] = mocked
@ -133,12 +153,12 @@ module LuckySneaks
# <tt>mock_and_assign</tt>. Accepts <tt>option[:size]</tt> which sets the size
# of the array (default is 3).
def mock_and_assign_collection(klass, *args)
options = args.dup.extract_options!
options = args.extract_options!
return_me = Array.new(options[:size] || 3) do
mocked = if options[:stub]
mock_model(klass, options[:stub])
self.respond_to?(:stub_model) ? stub_model(klass, options[:stub]) : mock_model(klass, options[:stub])
else
mock_model(klass)
self.respond_to?(:stub_model) ? stub_model(klass) : mock_model(klass)
end
yield mocked if block_given?
mocked
@ -150,8 +170,8 @@ module LuckySneaks
def do_render
if @the_template
render @the_template
elsif File.exists?(File.join(RAILS_ROOT, "app/views", self.class.description_text))
render self.class.description_text
elsif File.exists?(File.join(RAILS_ROOT, "app/views", class_description_text))
render class_description_text
else
error_message = "Cannot determine template for render. "
error_message << "Please define @the_template in the before block "
@ -182,6 +202,18 @@ module LuckySneaks
end
end
# Negative version of <tt>it_should_submit_to</tt>. See that method for more
# details.
def it_should_not_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should not submit to #{(hint || route)}" do
do_render
response.should_not submit_to(instance_eval(&route))
end
end
# Creates an expectation that the template uses Rails' <tt>form_for</tt> to generate
# the proper form action and method to create or update the specified object.
#
@ -190,41 +222,85 @@ module LuckySneaks
# not an instance variable, which would be nil in the scope of the example block.
# If you use namespacing for your <tt>form_for</tt>, you'll have to manually write out
# a similar spec.
def it_should_have_form_for(name)
def it_should_have_form_for(name, options = {})
it "should have a form_for(@#{name})" do
template.should_receive(:form_for).with(instance_for(name))
if options.empty?
template.should_receive(:form_for).with(instance_for(name))
else
template.should_receive(:form_for).with(instance_for(name), hash_including(options))
end
do_render
end
end
# Negative version of <tt>it_should_have_form_for</tt>. See that method for more
# details.
def it_should_not_have_form_for(name)
def it_should_not_have_form_for(name, options = {})
it "should not have a form_for(@#{name})" do
template.should_not_receive(:form_for).with(instance_for(name))
if options.empty?
template.should_not_receive(:form_for).with(instance_for(name))
else
template.should_not_receive(:form_for).with(instance_for(name), hash_including(options))
end
do_render
end
end
# Creates an expectation which calls <tt>allow_editing</tt> on the response
# from rendering the template. See that method for more details.
# Creates an expectation which calls <tt>allow_editing</tt> on the rendered
# template for each attribute specified. See the docs for <tt>allow_editing</tt>
# for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_editing</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_editing(name, method)
it "should allow editing of @#{name}##{method}" do
do_render
response.should allow_editing(instance_for(name), method)
def it_should_allow_editing(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should allow editing of @#{instance_name}##{attribute}" do
do_render
response.should allow_editing(instance_for(instance_name), attribute)
end
end
end
# Negative version of <tt>it_should_allow_editing</tt>. See that method for more
# details.
def it_should_not_allow_editing(name, method)
it "should not allow editing of @#{name}##{method}" do
do_render
response.should_not allow_editing(instance_for(name), method)
def it_should_not_allow_editing(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should not allow editing of @#{instance_name}##{attribute}" do
do_render
response.should_not allow_editing(instance_for(instance_name), attribute)
end
end
end
# Creates an expectation which calls <tt>allow_uploading</tt> on the rendered
# template for each attribute specified. See the docs for <tt>allow_uploading</tt>
# for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_uploading</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_uploading(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should allow editing of @#{instance_name}##{attribute}" do
do_render
response.should allow_uploading(instance_for(instance_name), attribute)
end
end
end
# Negative version of <tt>it_should_allow_uploading</tt>. See that method for more
# details.
def it_should_not_allow_uploading(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should not allow editing of @#{instance_name}##{attribute}" do
do_render
response.should_not allow_uploading(instance_for(instance_name), attribute)
end
end
end
@ -455,6 +531,47 @@ module LuckySneaks
alias it_should_have_link_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_or_link_to_delete_each it_should_link_to_delete_each
# Creates an expectation that the template should call <tt>render :partial</tt>
# with the specified template.
def it_should_render_partial(name)
it "should render :partial => '#{name}'" do
template.should_receive(:render).with(hash_including(:partial => name))
do_render
end
end
# Negative version of <tt>it_should_render_partial</tt>. See that method
# for more details.
def it_should_not_render_partial(name)
it "should not render :partial => '#{name}'" do
template.should_not_receive(:render).with(hash_including(:partial => name))
do_render
end
end
# Sets <tt>@the_template</tt> (for use in <tt>do_render</tt>) using the current
# example group description. Example:
#
# describe "users/index.haml.erb" do
# use_describe_for_template!
# # ...
# end
#
# This is equivalent to setting <tt>@the_template = "users/index.haml.erb"</tt>
# in a before block.
def use_describe_for_template!
template = self_description_text
if File.exists?(File.join(RAILS_ROOT, "app/views", template))
before(:each) do
@the_template = template
end
else
error_message = "You called use_describe_for_template! "
error_message << "but 'app/views/#{template}' does not exist. "
raise NameError, error_message
end
end
end
end
end
end

View file

@ -0,0 +1,15 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example <tt>before</tt> blocks to accomplish
# a whole lot of functionality with just a tiny bit of effort.
module ViewStubHelpers
# Shorthand for the following stub:
#
# template.stub!(:render).with(hash_including(:partial => anything))
def stub_partial_rendering!
template.stub!(:render).with(hash_including(:partial => anything))
end
end
end

View file

@ -1,10 +1,10 @@
# Let's make sure everyone else is loaded
require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
require File.expand_path(RAILS_ROOT + "/config/environment")
require 'spec'
require 'spec/rails'
begin
require 'ruby2ruby'
rescue
rescue LoadError
puts "-----"
puts "Attention: skinny_spec requires ruby2ruby for nicer route descriptions"
puts "It is highly recommended that you install it: sudo gem install ruby2ruby"