Install the `skinny_spec' plugin.

It's basically a set of rspec macro allowing you to write thing such as
`it_should_find_and_assign :users'.
See <http://github.com/rsl/skinny_spec/tree/master> for more informations.
It's recommended to install the `ruby2ruby' gem to have nicer error messages, but
it is not required to work.

@@ update README_FOR_DEVELOPERS accordingly.
This commit is contained in:
Simon Rozet 2008-06-22 20:26:30 +02:00
parent 60b986a5b9
commit 0376067cf4
33 changed files with 2531 additions and 0 deletions

2
vendor/plugins/skinny_spec/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
doc

267
vendor/plugins/skinny_spec/README.rdoc vendored Normal file
View file

@ -0,0 +1,267 @@
= Skinny Spec
Skinny Spec is a collection of spec helper methods designed to help trim the fat and DRY up
some of the bloat that sometimes results from properly specing your classes and templates.
== Requirements and Recommendations
Obviously you'll need to be using RSpec[http://github.com/dchelimsky/rspec/tree/master] and
Rspec-Rails[http://github.com/dchelimsky/rspec-rails/tree/master] as your testing framework.
Skinny Spec was originally designed [and best enjoyed] if you're using
Haml[http://github.com/nex3/haml/tree/master] and
make_resourceful[http://github.com/rsl/make_resourceful/tree/master] but will default to
ERb and a facsimile of Rails' default scaffolding [for the views and controllers, respectively]
if Haml and/or make_resourceful are not available. I recommend using them though. :)
In addition, Skinny Spec uses Ruby2Ruby to make nicer expectation messages and you'll want to
have that installed as well. It's not a dependency or anything but it <i>is</i> highly
recommended.
== Setup
Once you've installed the plugin in your app's vendor/plugins folder, you're ready to rock!
Skinny Spec includes itself into the proper RSpec classes so there's no configuration on your
part. Sweet!
== Usage
The simplest way to use Skinny Specs is to generate a resource scaffold:
script/generate skinny_scaffold User
This command takes the usual complement of attribute definitions like
<tt>script/generate scaffold</tt>. Then have a look at the generated files (particularly the
specs) to see what's new and different with Skinny Spec.
=== Controller Specs
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)
end
it_should_find_and_assign :users
it_should_render :template, "index"
end
# ...
describe "POST :create" do
describe "when successful" do
before(:each) do
@user = stub_create(User)
end
it_should_initialize_and_save :user
it_should_redirect_to { user_url(@user) }
end
# ...
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
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
mocking the main object of the method. To create mocks for other ancillary objects, please
use <tt>stub_find_all</tt>, <tt>stub_find_one</tt>, and <tt>stub_initialize</tt>. The reason
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:
describe "POST :create" do
def shared_request
post :create
end
describe "when successful" do
# ...
end
describe "when unsuccessful" do
# ...
end
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
example:
# Let's assume this controller is _not_ CategoriesController
# and that loading the categories isn't part of the default actions
describe "GET :index" do
before(:each) do
@categories = stub_find_all(Category)
end
it "should preload categories" do
Category.should_receive(:find).with(:all)
do_request
end
it "should assign @categories" do
do_request
assigns[:categories].should == @categories
end
end
Finally we get to the meat of the spec and of Skinny Specs itself: the actual expectations.
The first thing you'll notice is the use of example group (read: "describe" block) level methods
instead of the usual example (read: "it") blocks. Using this helper at the example group level
saves us three lines over using an example block. (If this isn't significant to you, this is
probably the wrong plugin for you as well. Sorry.) Note that none of these methods use the
instance variables defined in the "before" block because they are all nil at the example block
level. Let's look at a sample method to see how it works:
it_should_find_and_assign :users
This actually wraps two different expectations: one that <tt>User.should_receive(:find).with(:all)</tt>
and another that the instance variable <tt>@users</tt> is assigned with the return value from that finder call.
If you need to add more detailed arguments to the find, you can easily break this into two different
expectations like:
it_should_find :users, :limit => 2
it_should_assign :users
See the documentation for the <tt>it_should_find</tt> for more information. You might have guessed that
<tt>it_should_initialize_assign</tt> and <tt>it_should_render_template</tt> work in a similar
fashion and you'd be right. Again, see the documentation for these individual methods for more
information. Lots of information in those docs.
A useful helper method that doesn't appear in any of the scaffolding is <tt>with_default_restful_actions</tt>
which takes a block and evaluates it for each of the RESTful controller actions. Very useful for
spec'ing that these methods redirect to the login page when the user isn't logged in, for example. This
method is designed to be used inside an example like so:
describe "when not logged in" do
it "should redirect all requests to the login page" do
with_default_restful_actions do
response.should redirect_to(login_url)
end
end
end
Before we're through with the controller specs, let me point out one more important detail. In
order to use <tt>it_should_redirect_to</tt> we have to send the routing inside a block argument
there so it can be evaluated in the example context instead of the example group, where it
completely blows up. This methodology is used anywhere routing is referred to in a "skinny",
example group level spec.
=== View Specs
Now let's move to the view specs!
describe "/users/form.html.haml" do
before(:each) do
@user = mock_and_assign(User, :stub => {
:name => "foo",
:birthday => 1.week.ago,
:adult => false
})
end
it_should_have_form_for :user
it_should_allow_editing :user, :name
it_should_allow_editing :user, :birthday
it_should_allow_editing :user, :adult
it_should_link_to_show :user
it_should_link_to { users_path }
end
Like the special <tt>stub_index</tt> methods in the controller
specs, the view specs have a shorthand mock and stub helpers: <tt>mock_and_assign</tt> and
<tt>mock_and_assign_collection</tt>. These are well documented so please check them out.
There are also some really nice helper methods that I'd like point out. First is
<tt>it_should_have_form_for</tt>. This is a really good convenience wrapper that basically wraps
the much longer:
it "should use form_for to generate the proper form action and options" do
template.should_receive(:form_for).with(@user)
do_render
end
Next up is the <tt>it_should_allow_editing</tt> helper. I love this method the most because it
really helps DRY up that view spec while at the same time being amazingly unbrittle. Instead of
creating an expectation for a specific form element, this method creates a generalized expectation
that there's a form element with the <tt>name</tt> attribute set in such away that it will
generate the proper <tt>params</tt> to use in the controller to edit or create the instance.
Check out the docs and the source for more information on this. Also check out
<tt>it_should_have_form_element_for</tt> which is roughly equivalent for those times when you use
<tt>form_tag</tt> instead.
Finally let's look at those <tt>it_should_link_to_<i>controller_method</i></tt> helpers.
These methods (and there's one each for the controller methods
<tt>new</tt>, <tt>edit</tt>, <tt>show</tt>, and <tt>delete</tt>) point to instance variables
which you should be created in the "before" blocks with <tt>mock_and_assign</tt>. The other is
<tt>it_should_allow_editing</tt> which is likewise covered extensively in the documentation and
I will just point out here that, like <tt>it_should_link_to_edit</tt> and such, it takes a
symbol for the name of the instance variable it refers to and <i>additionally</i> takes
a symbol for the name of the attribute to be edited.
Also note that, when constructing a long form example, instead of defining an instance variable
for the name of the template and calling <tt>render @that_template</tt> you can simply call
<tt>do_render</tt> which takes the name of the template from the outermost example group where
it is customarily stated.
=== Model Specs
Skinny Spec adds a matcher for the various ActiveRecord associations. On the example group level
you call them like:
it_should_belong_to :manager
it_should_have_many :clients
Within an example you can call them on either the class or the instance setup in the
"before" block. These are equivalent:
@user.should belong_to(:group)
User.should belong_to(:group)
I've also added some very basic validation helpers like <tt>it_should_validate_presence_of</tt>,
<tt>it_should_validate_uniqueness_of</tt>, <tt>it_should_not_mass_assign</tt>. Please consult
the documentation for more information.
== Miscellaneous Notes
In the scaffolding, I have used my own idiomatic Rails usage:
* All controller actions which use HTML forms [<tt>new</tt>, <tt>edit</tt>, etc] use a shared
form and leverage <tt>form_for</tt> to its fullest by letting it create the appropriate
action and options.
* Some instances where you might expect link_to are button_to. This is to provide a common
interface element which can be styled the same instead of a mishmash of links and buttons and
inputs everywhere. To take full advantage of this, I usually override many of Rails' default
helpers with custom ones that all use actual HTML <tt>BUTTON</tt> elements which are much
easier to style than "button" typed <tt>INPUT</tt>. I've provided a text file in the
"additional" folder of this plugin which you can use in your ApplicationHelper. (I also
provide an optional override helper for the <tt>label</tt> method which uses
<tt>#titleize</tt> instead of <tt>humanize</tt> for stylistic reasons).
* Probably more that I can't think of.
== Credits and Thanks
Sections of this code were taken from or inspired by Rick Olsen's
rspec_on_rails_on_crack[http://github.com/technoweenie/rspec_on_rails_on_crack/tree/master].
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.

11
vendor/plugins/skinny_spec/Rakefile vendored Normal file
View file

@ -0,0 +1,11 @@
require 'rake'
require 'rake/rdoctask'
desc 'Generate documentation for the Skinny Spec plugin'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Skinny Spec'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,58 @@
# Please insert these into your ApplicationHelper
# Replacement for Rails' default submit_tag helper
# using HTML button element rather than HTML input element
def submit_tag(text, options = {})
content_tag :button, text, options.merge(:type => :submit)
end
# Replacement for Rails' default button_to helper
# using HTML button element rather than HTML input element
def button_to(name, options = {}, html_options = {})
html_options = html_options.stringify_keys
convert_boolean_attributes!(html_options, %w( disabled ))
method_tag = ''
if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
end
form_method = method.to_s == 'get' ? 'get' : 'post'
request_token_tag = ''
if form_method == 'post' && protect_against_forgery?
request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
end
if confirm = html_options.delete("confirm")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end
url = options.is_a?(String) ? options : self.url_for(options)
name ||= url
html_options.merge!("type" => "submit", "value" => name)
"<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
# Replacement for Rails' default button_to_function helper
# using HTML button element rather than HTML input element
def button_to_function(name, *args, &block)
html_options = args.extract_options!
function = args[0] || ''
html_options.symbolize_keys!
function = update_page(&block) if block_given?
content_tag(:button, name, html_options.merge({
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
}))
end
# Replacement for Rails' default label helper
# using String#titleize rather than String#humanize
def label(object_name, method, text = nil, options = {})
text ||= method.to_s[].titleize
super
end

View file

@ -0,0 +1,93 @@
class SkinnyScaffoldGenerator < Rails::Generator::NamedBase
attr_reader :controller_class_path, :controller_file_path, :controller_class_nesting,
:controller_class_nesting_depth, :controller_class_name, :controller_underscore_name,
:controller_plural_name, :template_language
alias_method :controller_file_name, :controller_underscore_name
alias_method :controller_singular_name, :controller_file_name
alias_method :controller_table_name, :controller_plural_name
def initialize(runtime_args, runtime_options = {})
super
base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@name.pluralize)
@controller_class_name_without_nesting, @controller_underscore_name, @controller_plural_name = inflect_names(base_name)
if @controller_class_nesting.empty?
@controller_class_name = @controller_class_name_without_nesting
else
@controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
end
end
def manifest
record do |m|
# Check for class naming collisions
m.class_collisions controller_class_path, "#{controller_class_name}Controller", "#{controller_class_name}Helper"
m.class_collisions class_path, "#{class_name}"
# # Controller, helper, and views directories
m.directory File.join('app', 'views', controller_class_path, controller_file_name)
m.directory File.join('spec', 'views', controller_class_path, controller_file_name)
m.directory File.join('app', 'helpers', controller_class_path)
m.directory File.join('spec', 'helpers', controller_class_path)
m.directory File.join('app', 'controllers', controller_class_path)
m.directory File.join('spec', 'controllers', controller_class_path)
m.directory File.join('app', 'models', class_path)
m.directory File.join('spec', 'models', class_path)
# Views
@template_language = defined?(Haml) ? "haml" : "erb"
%w{index show form}.each do |action|
m.template "#{action}.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}")
m.template "#{action}.html_spec.rb",
File.join('spec/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}_spec.rb")
end
m.template "index_partial.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}")
m.template 'index_partial.html_spec.rb',
File.join('spec/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}_spec.rb")
# Helper
m.template 'helper.rb',
File.join('app/helpers', controller_class_path, "#{controller_file_name}_helper.rb")
m.template 'helper_spec.rb',
File.join('spec/helpers', controller_class_path, "#{controller_file_name}_helper_spec.rb")
# Controller
m.template 'controller.rb',
File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
m.template 'controller_spec.rb',
File.join('spec/controllers', controller_class_path, "#{controller_file_name}_controller_spec.rb")
# Model
m.template 'model.rb',
File.join('app/models', class_path, "#{file_name}.rb")
m.template 'model_spec.rb',
File.join('spec/models', class_path, "#{file_name}_spec.rb")
# Routing
m.route_resources controller_file_name
unless options[:skip_migration]
m.migration_template(
'migration.rb', 'db/migrate',
:assigns => {
:migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}",
:attributes => attributes
},
:migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
)
end
end
end
protected
def banner
"Usage: #{$0} skinny_scaffold ModelName [field:type, field:type]"
end
def model_name
class_name.demodulize
end
end

View file

@ -0,0 +1,105 @@
class <%= controller_class_name %>Controller < ApplicationController
<%- if defined?(Resourceful::Maker) -%>
make_resourceful do
actions :all
# Let's get the most use from form_for and share a single form here!
response_for :new, :edit do
render :template => "<%= plural_name %>/form"
end
response_for :create_fails, :update_fails do
flash[:error] = "There was a problem!"
render :template => "<%= plural_name %>/form"
end
end
<%- else -%>
# GET /<%= table_name %>
# GET /<%= table_name %>.xml
def index
@<%= table_name %> = <%= class_name %>.find(:all)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @<%= table_name %> }
end
end
# GET /<%= table_name %>/1
# GET /<%= table_name %>/1.xml
def show
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/new
# GET /<%= table_name %>/new.xml
def new
@<%= file_name %> = <%= class_name %>.new
respond_to do |format|
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/1/edit
def edit
@<%= file_name %> = <%= class_name %>.find(params[:id])
render :template => "<%= plural_name %>/form"
end
# POST /<%= table_name %>
# POST /<%= table_name %>.xml
def create
@<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>])
respond_to do |format|
if @<%= file_name %>.save
flash[:notice] = '<%= class_name %> was successfully created.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { render :xml => @<%= file_name %>, :status => :created, :location => @<%= file_name %> }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# PUT /<%= table_name %>/1
# PUT /<%= table_name %>/1.xml
def update
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
if @<%= file_name %>.update_attributes(params[:<%= file_name %>])
flash[:notice] = '<%= class_name %> was successfully updated.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { head :ok }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /<%= table_name %>/1
# DELETE /<%= table_name %>/1.xml
def destroy
@<%= file_name %> = <%= class_name %>.find(params[:id])
@<%= file_name %>.destroy
respond_to do |format|
flash[:notice] = '<%= class_name %> was successfully deleted.'
format.html { redirect_to(<%= table_name %>_url) }
format.xml { head :ok }
end
end
<%- end -%>
end

View file

@ -0,0 +1,116 @@
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 %>)
end
it_should_find_and_assign :<%= plural_name %>
it_should_render_template "index"
end
describe "GET :new" do
before(:each) do
@<%= singular_name %> = stub_new(<%= class_name %>)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "POST :create" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>)
end
it_should_initialize_and_save :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>, :return => :false)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "GET :show" do
before(:each) do
@<%= singular_name %> = stub_show(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "show"
end
describe "GET :edit" do
before(:each) do
@<%= singular_name %> = stub_edit(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "PUT :update" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>)
end
it_should_find_and_update :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>, :return => :false)
end
it_should_find_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "DELETE :destroy" do
before(:each) do
@<%= singular_name %> = stub_destroy(<%= class_name %>)
end
it_should_find_and_destroy :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= plural_name %>_url }
end
end

View file

@ -0,0 +1,25 @@
<h1><%= singular_name %>.new_record? ? "New" : "Edit" %> <%= model_name %></h1>
<%% form_for(@<%= singular_name %>) do |f| %>
<div id="form_errors">
<%%= f.error_messages %>
</div>
<%- if attributes.blank? -%>
<p>Add your form elements here, please!</p>
<%- else -%>
<%- attributes.each do |attribute| -%>
<p>
<%%= f.label :<%= attribute.name %> %><br />
<%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
</p>
<%- end -%>
<%- end -%>
<div id="commands">
<%%= submit_tag "Save" %>
<div id="navigation_commands">
<%% unless @<%= singular_name %>.new_record? -%>
<%%= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost." %>
<%% end -%>
<%%= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes" %>
</div>
</div>
<%% end -%>

View file

@ -0,0 +1,18 @@
%h1== #{@<%= singular_name %>.new_record? ? "New" : "Edit"} #{<%= model_name %>}
- form_for @<%= singular_name %> do |f|
#form_errors= f.error_messages
<% if attributes.blank? -%>
%p Add your form elements here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
= f.label :<%= attribute.name %>
= f.<%= attribute.field_type %> :<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= submit_tag "Save"
#navigation_commands
- unless @<%= singular_name %>.new_record?
= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost."
= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes"

View file

@ -0,0 +1,41 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/form.html.<%= template_language %>" do
before(:each) do
@<%= 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
<% 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 -%>
})
end
it_should_have_form_for :<%= singular_name %>
<% if attributes.blank? -%>
# Add specs for editing attributes here, please! Like this:
#
# it_should_allow_editing :<%= singular_name %>, :foo
<% else -%>
<%- attributes.each do |attribute| -%>
it_should_allow_editing :<%= singular_name %>, :<%= attribute.name %>
<%- end -%>
<% end -%>
it_should_link_to_show :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

View file

@ -0,0 +1,2 @@
module <%= controller_class_name %>Helper
end

View file

@ -0,0 +1,5 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../spec_helper'
describe <%= controller_class_name %>Helper do
# Add your specs here or remove this file completely, please!
end

View file

@ -0,0 +1,31 @@
<h1><%= model_name %> List</h1>
<table>
<%%- if @<%= plural_name %>.empty? -%>
<tbody>
<tr class="empty">
<td>There are no <%= plural_name.humanize.downcase %></td>
</tr>
</tbody>
<%%- else -%>
<thead>
<tr>
<%- if attributes.blank? -%>
<th><!-- Generic display column --></th>
<%- else -%>
<%- attributes.each do |attribute| -%>
<th><%= attribute.name.titleize %></th>
<%- end -%>
<%- end -%>
<th class="show"><!-- "Show" link column --></th>
<th class="edit"><!-- "Edit" link column --></th>
<th class="delete"><!-- "Delete" link column --></th>
</tr>
</thead>
<tbody>
<%%= render :partial => @<%= plural_name %> %>
</tbody>
<%%- end -%>
</table>
<div id="commands">
<%%= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,23 @@
%h1 <%= model_name %> List
%table
- if @<%= plural_name %>.empty?
%tbody
%tr.empty
%td== There are no <%= plural_name.humanize.downcase %>
- else
%thead
%tr
<% if attributes.blank? -%>
%th= # Generic display column
<% else -%>
<%- attributes.each do |attribute| -%>
%th <%= attribute.name.titleize %>
<%- end -%>
<% end -%>
%th.show= # 'Show' link column
%th.edit= # 'Edit' link column
%th.delete= # 'Delete' link column
%tbody
= render :partial => @<%= plural_name %>
#commands
= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get"

View file

@ -0,0 +1,15 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
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 %>
end
it "should render :partial => @<%= plural_name %>" do
template.expect_render :partial => @<%= plural_name %>
do_render
end
it_should_link_to_new :<%= singular_name %>
end

View file

@ -0,0 +1,12 @@
<tr class="<%%= cycle("odd", "even") %>">
<% if attributes.blank? -%>
<td><%= model_name %> #<%%= <%= singular_name %>.id %></td>
<% else -%>
<%- attributes.each do |attribute| -%>
<td><%%=h <%= singular_name %>.<%= attribute.name %> %></td>
<%- end %>
<% end -%>
<td class="show"><%%= button_to "Show", <%= singular_name %>, :method => "get" %></td>
<td class="edit"><%%= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get" %></td>
<td class="delete"><%%= button_to "Delete", <%= singular_name %>, :method => "delete" %></td>
</tr>

View file

@ -0,0 +1,11 @@
%tr{:class => cycle("odd", "even")}
<% if attributes.blank? -%>
%td== <%= model_name %> #{<%= singular_name %>.id}
<% else -%>
<%- attributes.each do |attribute| -%>
%td=h <%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
%td.show= button_to "Show", <%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.edit= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.delete= button_to "Delete", <%= singular_name %>, :method => "delete"

View file

@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/_<%= singular_name %>.html.<%= template_language %>" do
before(:each) do
@<%= 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
<% 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 ? "," : "" %>
<%- end -%>
<%- end -%>
<% end -%>
})
template.stub!(:<%= singular_name %>).and_return(@<%= singular_name %>)
end
it_should_link_to_show :<%= singular_name %>
it_should_link_to_edit :<%= singular_name %>
it_should_link_to_delete :<%= singular_name %>
end

View file

@ -0,0 +1,14 @@
class <%= migration_name %> < ActiveRecord::Migration
def self.up
create_table :<%= table_name %>, :force => true do |t|
<% attributes.each do |attribute| -%>
t.column :<%= attribute.name %>, :<%= attribute.type %>
<% end -%>
t.timestamps
end
end
def self.down
drop_table :<%= table_name %>
end
end

View file

@ -0,0 +1,2 @@
class <%= class_name %> < ActiveRecord::Base
end

View file

@ -0,0 +1,25 @@
require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../spec_helper'
describe <%= class_name %> do
def valid_attributes(args = {})
{
# Add valid attributes for building your model instances here!
}.merge(args)
end
before(:each) do
@<%= singular_name %> = <%= class_name %>.new
end
after(:each) do
@<%= singular_name %>.destroy unless @<%= singular_name %>.new_record?
end
# Add your model specs here, please!
# And don't forget about the association matchers built-in to skinny_spec like:
#
# it_should_have_many :foos
# it_should_validate_presence_of :bar
#
# Check out the docs for more information.
end

View file

@ -0,0 +1,15 @@
<h1>Show <%= model_name %></h1>
<% if attributes.blank? -%>
<p>Add your customized markup here, please!</p>
<% else -%>
<%- attributes.each do |attribute| -%>
<p>
<label><%= attribute.name.titleize %>:</label>
<%%=h @<%= singular_name %>.<%= attribute.name %> %>
</p>
<%- end -%>
<% end -%>
<div id="commands">
<%%= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get" %>
<%%= button_to "Back to List", <%= plural_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,13 @@
%h1== Show #{<%= model_name %>}
<% if attributes.blank? -%>
%p Add your customized markup here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
%label <%= attribute.name.titleize %>:
=h @<%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get"
= button_to "Back to List", <%= plural_name %>_path, :method => "get"

View file

@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/show.html.<%= template_language %>" do
before(:each) do
<% if attributes.blank? -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>)
<% else -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<%- 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 ? "," : "" %>
<%- end -%>
<%- end -%>
})
<% end -%>
end
# Add your specs here, please! But remember not to make them brittle
# by specing specing specific HTML elements and classes.
it_should_link_to_edit :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

3
vendor/plugins/skinny_spec/init.rb vendored Normal file
View file

@ -0,0 +1,3 @@
if RAILS_ENV == "test"
require "skinny_spec"
end

View file

@ -0,0 +1,46 @@
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
# 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
# Let any other error rise!
end
# Returns instance variable for the specified name. Example:
#
# instance_for("foo") # => @foo
def instance_for(name)
instance_variable_get("@#{name.to_s.underscore}")
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.
def have_link_to(path)
have_tag("a[href='#{path}']")
end
# Returns dummy value for specified attribute based on the datatype expected for that
# attribute.
def dummy_value_for(instance, attribute)
if datatype = instance.column_for_attribute(attribute)
actual = instance.send(attribute)
case datatype.type
when :string, :text
actual == "foo" ? "bar" : "food"
when :integer, :float, :decimal
actual == 108 ? 815 : 108
when :boolean
actual ? false : true
when :date, :datetime, :time, :timestamp
actual == 1.week.ago ? 2.years.ago : 1.week.ago
end
end
end
end
end

View file

@ -0,0 +1,67 @@
module LuckySneaks
module ControllerRequestHelpers # :nodoc:
def self.included(base)
base.extend ExampleGroupMethods
end
private
def define_implicit_request(method)
@controller_method = method
@implicit_request = case method
when :index, :new, :show, :edit
proc { get method, params }
when :create
proc { post :create, params }
when :update
proc { put :update, params }
when :destroy
proc { put :destroy, params }
end
end
def eval_request
instance_eval &self.class.instance_variable_get("@the_request")
rescue ArgumentError # missing block
try_shared_request_definition
end
alias do_request eval_request
def try_shared_request_definition
shared_request
rescue NameError
if @implicit_request
try_implicit_request
else
error_message = "Could not determine request definition for 'describe' context. "
error_message << "Please use define_request or define a shared_request."
raise ArgumentError, error_message
end
end
def try_implicit_request
@implicit_request.call
end
def get_response(&block)
eval_request
block.call(response) if block_given?
response
end
module ExampleGroupMethods
# Defines a request at the example group ("describe") level to be evaluated in the examples. Example:
#
# define_request { get :index, params }
#
# <b>Note:</b> The following methods all define implicit requests: <tt>stub_index</tt>, <tt>stub_new</tt>,
# <tt>stub_create</tt>, <tt>stub_show</tt>, <tt>stub_edit</tt>, <tt>stub_update</tt>, and
# <tt>stub_destroy</tt>. Using them in your <tt>before</tt> blocks will allow you to forego
# defining explicit requests using <tt>define_request</tt>. See
# LuckySneaks::ControllerStubHelpers for information on these methods.
def define_request(&block)
raise ArgumentError, "Must provide a block to define a request!" unless block_given?
@the_request = block
end
end
end
end

View file

@ -0,0 +1,435 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
module ControllerSpecHelpers # :nodoc:
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ControllerRequestHelpers
include LuckySneaks::ControllerStubHelpers
def self.included(base)
base.extend ExampleGroupMethods
base.extend ControllerRequestHelpers::ExampleGroupMethods
end
# Evaluates the specified block for each of the RESTful controller methods.
# This is useful to spec that all controller methods redirect when no user is
# logged in.
def with_default_restful_actions(params = {}, &block)
{
:get => :index,
:get => :new,
:post => :create
}.each do |method_id, message|
self.send method_id, message, params
block.call
end
{
:get => :edit,
:put => :update,
:delete => :destroy
}.each do |method_id, message|
if params[:before]
params.delete(:before).call
end
# Presuming any id will do
self.send method_id, message, params.merge(:id => 1)
block.call
end
end
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
args << argument unless argument.nil?
end
args << options unless options.empty?
if args.empty?
return_value = class_for(name).send(method)
class_for(name).should_receive(method).and_return(return_value)
else
return_value = class_for(name).send(method, *args)
class_for(name).should_receive(method).with(*args).and_return(return_value)
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
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations.
module ExampleGroupMethods
# 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")
#
# <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.
def it_should_find(name, *args)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should find #{name_message}" do
options = args.extract_options!
# Blech!
argument = if param = params[options.delete(:params)]
param.to_s
else
if args.first
args.first
elsif (instance = instance_variable_get("@#{name}")).is_a?(ActiveRecord::Base)
instance.id.to_s
else
:all
end
end
create_ar_class_expectation name, :find, argument, options
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.new</tt>.
# Takes optional <tt>params</tt> for the initialization arguments. Example
#
# it_should_initialize :foo # => Foo.should_receive(:new)
# it_should_initialize :foo, :params => :bar # => Foo.should_receive(:new).with(params[:bar])
# it_should_initialize :foo, :bar => "baz" # => Foo.should_receive(:new).with(:bar => "baz")
def it_should_initialize(name, options = {})
it "should initialize a #{name}" do
create_ar_class_expectation name, :new, params[options.delete(:params)], options
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#save</tt> on the
# named instance. Example:
#
# it_should_save :foo # => @foo.should_receive(:save).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>save</tt> call. Use <tt>it_should_assign</tt>
# instead, to verify that the instance is captured in an instance variable for the inevitable re-rendering
# of the form template.
def it_should_save(name)
it "should save the #{name}" do
create_positive_ar_instance_expectation name, :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:
#
# it_should_update :foo # => @foo.should_receive(:update_attributes).and_return(true)
# it_should_update :foo, :params => :bar # => @foo.should_receive(:update_attributes).with(params[:bar]).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>update_attributes</tt> call. Use
# <tt>it_should_assign</tt> instead, to verify that the instance is captured in an instance variable
# 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]]
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#destroy</tt> on the named
# instance. Example:
#
# it_should_destroy :foo # => @foo.should_receive(:destroy).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>destroy</tt> call. Use
# <tt>it_should_assign</tt> instead, if you need to verify that the instance is captured in an instance
# variable if it is re-rendered somehow. This is probably a really edge use case.
def it_should_destroy(name, options = {})
it "should delete the #{name}" do
create_positive_ar_instance_expectation name, :destroy
eval_request
end
end
# Creates expectation[s] that the controller method should assign the specified
# instance variables along with any specified values. Examples:
#
# it_should_assign :foo # => assigns[:foo].should == @foo
# it_should_assign :foo => "bar" # => assigns[:foo].should == "bar"
# it_should_assign :foo => :nil # => assigns[:foo].should be_nil
# it_should_assign :foo => :not_nil # => assigns[:foo].should_not be_nil
# it_should_assign :foo => :undefined # => controller.send(:instance_variables).should_not include("@foo")
#
# Very special thanks to Rick Olsen for the basis of this code. The only reason I even
# redefine it at all is purely an aesthetic choice for specs like "it should foo"
# over ones like "it foos".
def it_should_assign(*names)
names.each do |name|
if name.is_a?(Symbol)
it_should_assign name => name
elsif name.is_a?(Hash)
name.each do |key, value|
it_should_assign_instance_variable key, value
end
end
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_assign name
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.
#
# <b>Note:</b> This method is used for controller methods like <tt>new</tt>, where the instance
# is initialized without being saved (this includes failed <tt>create</tt> requests).
# If you want to spec that the controller method successfully saves the instance,
# 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_assign name
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.
#
# <b>Note:</b> This method is used for controller methods like <tt>create</tt>, where the instance
# is initialized and successfully saved. If you want to spec that the instance is created
# 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_save name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_update</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.
#
# <b>Note:</b> This method is used for controller methods like <tt>update</tt>, where the
# instance is loaded from the database and successfully saved. If you want to spec that the
# 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_update name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_destroy</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_destroy(*names)
names.each do |name|
it_should_find name
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.
def it_should_set(collection, key, value = nil, &block)
it "should set #{collection}[:#{key}]" do
# Allow flash.now[:foo] to remain in the flash
flash.stub!(:sweep) if collection == :flash
eval_request
if value
if value == :nil
self.send(collection)[key].should be_nil
else
self.send(collection)[key].should == value
end
elsif block_given?
self.send(collection)[key].should == block.call
else
self.send(collection)[key].should_not be_nil
end
end
end
# Wraps <tt>it_should_set :flash</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_flash(name, value = nil, &block)
it_should_set :flash, name, value, &block
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.
def it_should_set_session(name, value = nil, &block)
it_should_set :session, name, value, &block
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>,
# <tt>it_should_render_formatted</tt>, and <tt>it_should_render_nothing</tt>.
def it_should_render(render_method, *args)
send "it_should_render_#{render_method}", *args
end
# Creates an expectation that the controller method renders the specified template.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_template(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' template" do
eval_request
response.should render_template(name)
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified partial.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_partial(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' partial" do
controller.expect_render(:partial => name)
eval_request
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified record via <tt>to_xml</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_xml(record = nil, options = {}, &block)
it_should_render_formatted :xml, record, options, &block
end
# Creates an expectation that the controller method renders the specified record via <tt>to_json</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_json(record = nil, options = {}, &block)
it_should_render_formatted :json, record, options, &block
end
# Called internally by <tt>it_should_render_xml</tt> and <tt>it_should_render_json</tt>
# but should not really be called much externally unless you have defined your own
# formats with a matching <tt>to_foo</tt> method on the record.
#
# Which is probably never.
def it_should_render_formatted(format, record = nil, options = {}, &block)
create_status_expectation options[:status] if options[:status]
it "should render #{format.inspect}" do
if record.is_a?(Hash)
options = record
record = nil
end
if record.nil? && !block_given?
raise ArgumentError, "it_should_render must be called with either a record or a block and neither was given."
else
if record
pieces = record.to_s.split(".")
record = instance_variable_get("@#{pieces.shift}")
record = record.send(pieces.shift) until pieces.empty?
end
block ||= proc { record.send("to_#{format}") }
get_response do |response|
response.should have_text(block.call)
end
end
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method returns a blank page. You'd already
# know when and why to use this so I'm not typing it out.
def it_should_render_nothing(options = {})
create_status_expectation options[:status] if options[:status]
it "should render :nothing" do
get_response do |response|
response.body.strip.should be_blank
end
end
end
# Creates an expectation that the controller method redirects to the specified destination. Example:
#
# it_should_redirect_to { foos_url }
#
# <b>Note:</b> This method takes a block to evaluate the route in the example
# context rather than the example group context.
def it_should_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should redirect to #{(hint || route)}" do
eval_request
response.should redirect_to(instance_eval(&route))
end
end
private
def it_should_assign_instance_variable(name, value)
expectation_proc = case value
when :nil
proc { assigns[name].should be_nil }
when :not_nil
proc { assigns[name].should_not be_nil }
when :undefined
proc { controller.send(:instance_variables).should_not include("@{name}") }
when Symbol
if (instance_variable = instance_variable_get("@#{name}")).nil?
proc { assigns[name].should_not be_nil }
else
proc { assigns[name].should == instance_variable }
end
else
proc { assigns[name].should == value }
end
it "should #{value == :nil ? 'not ' : ''}assign @#{name}" do
eval_request
instance_eval &expectation_proc
end
end
end
end
end

View file

@ -0,0 +1,199 @@
module LuckySneaks # :nodoc:
# 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. The methods which correspond
# to the controller methods perform the most duties as they create the mock_model instances,
# stub out all the necessary methods, and also create implicit requests to DRY up your spec
# 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>: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.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# Any additional options will be passed as arguments to the class find.
# You will want to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
def stub_find_all(klass, options = {})
returning(Array.new(options[:size] || 3){mock_model(klass)}) do |collection|
stub_out klass, options.delete(:stub)
if format = options.delete(:format)
stub_formatted collection, format
params[:format] = format
end
if options.empty?
klass.stub!(:find).with(:all).and_return(collection)
else
klass.stub!(:find).with(:all, options).and_return(collection)
end
end
end
# Alias for <tt>stub_find_all</tt> but additionally defines an implicit request <tt>get :index</tt>.
def stub_index(klass, options = {})
define_implicit_request :index
stub_find_all klass, options
end
# Stubs out <tt>new</tt> method and returns a <tt>mock_model</tt> instance marked as a new record.
# 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
#
# It also accepts some options used to stub out <tt>save</tt> with a specified <tt>true</tt>
# or <tt>false</tt> but you should be using <tt>stub_create</tt> in that case.
def stub_initialize(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if format = options[:format]
stub_formatted member, format
params[:format] = format
end
klass.stub!(:new).and_return(member)
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)
end
end
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>get :new</tt>.
def stub_new(klass, options = {})
define_implicit_request :new
stub_initialize klass, options
end
# 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.
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
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
#
# 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.
#
# <b>Note:</b> The option <tt>:stub_ar</tt> is used internally by <tt>stub_update</tt>
# and <tt>stub_destroy</tt>. If you need to stub <tt>update_attributes</tt> or
# <tt>destroy</tt> you should be using the aforementioned methods instead.
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]
end
if options[:current_object]
params[:id] = member.id
if options[:stub_ar]
stub_ar_method member, options[:stub_ar], options[:return]
end
end
klass.stub!(:find).with(member.id.to_s).and_return(member)
end
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :show</tt>.
def stub_show(klass, options = {})
define_implicit_request :show
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :edit</tt>.
def stub_edit(klass, options = {})
define_implicit_request :edit
stub_find_one klass, options.merge(:current_object => true)
end
# 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.
def stub_update(klass, options = {})
define_implicit_request :update
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :update_attributes)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>delete :destroy</tt>
# and stubs out the <tt>destroy</tt> method on the instance as well.
def stub_destroy(klass, options = {})
define_implicit_request :destroy
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :destroy)
end
# Stubs <tt>to_xml</tt> or <tt>to_json</tt> respectively based on <tt>format</tt> argument.
def stub_formatted(object, format)
return unless format
object.stub!("to_#{format}").and_return("#{object.class} formatted as #{format}")
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?
def stub_out(object, stubs = {})
return if stubs.nil?
stubs.each do |method, value|
if value
object.stub!(method).and_return(value)
else
object.stub!(method)
end
end
end
# 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)
end
end
end

View file

@ -0,0 +1,326 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# 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
#
# <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 ModelSpecHelpers
include LuckySneaks::CommonSpecHelpers
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
class AssociationMatcher # :nodoc:
def initialize(associated, macro)
@associated = associated
@macro = macro
@options = {}
end
def matches?(main_model)
unless main_model.respond_to?(:reflect_on_association)
if main_model.class.respond_to?(:reflect_on_association)
main_model = main_model.class
else
@not_model = main_model
return false
end
end
if @association = main_model.reflect_on_association(@associated)
@options.all?{|k, v| @association.options[k] == v ||
[@association.options[k]] == v} # Stupid to_a being obsoleted!
end
end
def failure_message
if @not_model
" expected: #{@not_model} to be a subclass of ActiveRecord::Base class, but was not"
elsif @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)}, but the association does not exist"
end
end
def negative_failure_message
if @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)} to not occur but it does"
end
end
# The following public methods are chainable extensions on the main matcher
# Examples:
#
# Foo.should have_many(:bars).through(:foobars).with_dependent(:destroy)
# Bar.should belong_to(:baz).with_class_name("Unbaz")
def through(through_model)
@options[:through] = through_model
self
end
def and_includes(included_models)
@options[:include] = included_models
self
end
def and_extends(*modules)
@options[:extends] = modules
self
end
def with_counter_cache(counter_cache = false)
if counter_cache
@options[:counter_cache] = counter_cache
end
self
end
def uniq(*irrelevant_args)
@options[:uniq] = true
self
end
alias and_is_unique uniq
alias with_unique uniq
def polymorphic(*irrelevant_args)
@options[:polymorphic] = true
self
end
alias and_is_polymorphic polymorphic
alias with_polymorphic polymorphic
def as(interface)
@options[:as] = interface
end
# Use this to just specify the options as a hash.
# Note: It will completely override any previously set options
def with_options(options = {})
options.each{|k, v| @options[k] = v}
self
end
private
# Takes care of methods like with_dependent(:destroy)
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if method_name =~ /^with_(.*)/
@options[$1.to_sym] = args
self
else
super method_id, *args, &block
end
end
def association_with(options)
option_string = (options.nil? || options.empty?) ? "" : options.inspect
unless option_string.blank?
option_string.sub! /^\{(.*)\}$/, ', \1'
option_string.gsub! /\=\>/, ' => '
end
"#{@macro} :#{@associated}#{option_string}"
end
end
# Creates matcher that checks if the receiver has a <tt>belongs_to</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def belong_to(model)
AssociationMatcher.new model, :belongs_to
end
# Creates matcher that checks if the receiver has a <tt>have_one</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_one(model)
AssociationMatcher.new model, :has_one
end
# Creates matcher that checks if the receiver has a <tt>have_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_many(models)
AssociationMatcher.new models, :has_many
end
# Creates matcher that checks if the receiver has a <tt>have_and_belong_to_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_and_belong_to_many(models)
AssociationMatcher.new models, :has_and_belongs_to_many
end
private
def class_or_instance
@model_spec_class_or_instance ||= class_for(self.class.description_text) || instance
end
def instance
@model_spec_instance ||= instance_for(self.class.description_text)
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
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ModelSpecHelpers for more information.
module ExampleGroupLevelMethods
# Creates an expectation that the current model being spec'd has a <tt>belongs_to</tt>
# association with the specified model.
#
# <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)
it "should belong to a #{model}" do
class_or_instance.should belong_to(model)
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_one</tt>
# association with the specified model.
#
# <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)
it "should have one #{model}" do
class_or_instance.should have_one(model)
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_many</tt>
# association with the specified model.
#
# <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)
it "should have many #{models}" do
class_or_instance.should have_many(models)
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.
#
# <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)
it "should have and belong to many #{models}" do
class_or_instance.should have_and_belong_to_many(models)
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 = ActiveRecord::Errors.default_error_messages[:blank])
it "should not be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should 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 = ActiveRecord::Errors.default_error_messages[:not_a_number])
it "should validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should 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 = ActiveRecord::Errors.default_error_messages[:confirmation])
it "should validate confirmation of #{attribute}" do
dummy_value = dummy_value_for(instance, attribute) || "try a string"
instance.send "#{attribute}=", dummy_value
instance.send "#{attribute}_confirmation=", dummy_value.succ
instance.errors_on(attribute).should include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_uniqueness_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
#
# <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 = ActiveRecord::Errors.default_error_messages[:taken])
it "should validate #{attribute} confirmation" do
previous_instance = class_for(self.class.description_text).create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should 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|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
instance.errors_on(attribute).should == []
end
end
end
# Creates an expectation that the current model being spec'd does not accept 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.
# Takes an optional argument <tt>:message => "some custom error messsage"</tt> for
# spec'ing the actual error message.
def it_should_not_accept_as_valid(attribute, *values)
options = values.extract_options!
values.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should not accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
if options[:message]
instance.errors_on(attribute).should include(options[:message])
else
instance.should have_at_least(1).errors_on(attribute)
end
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)
it "should not allow mass-assignment of #{attribute}" do
lambda {
instance.send :attributes=, {attribute => dummy_value_for(instance, attribute)}
}.should_not change(instance, attribute)
end
end
end
end
end

View file

@ -0,0 +1,460 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# 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
module ViewSpecHelpers
include LuckySneaks::CommonSpecHelpers
include ActionController::PolymorphicRoutes
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element with
# its <tt>action</tt> attribute set to the specified path.
def submit_to(path)
have_tag("form[action=#{path}]")
end
# Wraps a matcher that checks is 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:
#
# response.should allow_editing(@foo, "bar")
#
# can be satisfied by any of the following HTML elements:
#
# <input name="foo[bar]" type="text" />
# <input name="foo[bar]" type="checkbox" />
# <input name="foo[bar_ids][]" type="checkbox" />
# <select name="foo[bar]"></select>
# <textarea name="foo[bar]"></textarea>
def allow_editing(instance, attribute)
instance_name = instance.class.name.underscore.downcase
if instance.send(attribute).is_a?(Time)
have_tag(
"input[name='#{instance_name}[#{attribute}]'],
select[name=?]", /#{instance_name}\[#{attribute}\(.*\)\]/
)
else
have_tag(
"input[type='text'][name='#{instance_name}[#{attribute}]'],
input[type='password'][name='#{instance_name}[#{attribute}]'],
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][]']"
)
end
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>
# element whose <tt>action</tt> attribute is set to the specified path.
def have_link_or_button_to(path)
have_tag(
"a[href='#{path}'],
form[action='#{path}'] input,
form[action='#{path}'] button"
)
end
alias have_link_to have_link_or_button_to
alias have_button_to have_link_or_button_to
# Wraps <tt>have_link_or_button_to new_polymorphic_path<tt> for the specified class which
# corresponds with the <tt>new</tt> method of the controller.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def have_link_or_button_to_new(name)
have_link_or_button_to new_polymorphic_path(name.is_a?(ActiveRecord::Base) ? name : class_for(name))
end
# 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)
end
alias have_link_to_show have_link_or_button_to_show
alias have_button_to_show have_link_or_button_to_show
# Wraps <tt>have_link_or_button_to edit_polymorphic_path(instance)<tt> which
# corresponds with the <tt>edit</tt> method of the controller.
def have_link_or_button_to_edit(instance)
have_link_or_button_to edit_polymorphic_path(instance)
end
alias have_link_to_edit have_link_or_button_to_edit
alias have_button_to_edit have_link_or_button_to_edit
# Wraps a matcher that checks if the receiver contains the HTML created by Rails'
# <tt>button_to</tt> helper: to wit, a <tt>FORM</tt> element whose <tt>action</tt>
# attribute is pointed at the <tt>polymorphic_path</tt> of the instance
# and contains an <tt>INPUT</tt> named "_method" with a value of "delete".
def have_button_to_delete(instance)
path = polymorphic_path(instance)
have_tag(
"form[action='#{path}'] input[name='_method'][value='delete'] + input,
form[action='#{path}'] input[name='_method'][value='delete'] + button"
)
end
# Creates a <tt>mock_model</tt> instance and adds it to the <tt>assigns</tt> collection
# using either the name passed as the first argument or the underscore version
# of its class name. Accepts optional arguments to stub out additional methods
# (and their return values) on the <tt>mock_model</tt> instance. Example:
#
# mock_and_assign(Foo, :stub => {:bar => "bar"})
#
# is the same as running <tt>assigns[:foo] = mock_model(Foo, :bar => "bar")</tt>.
#
# mock_and_assign(Foo, "special_foo", :stub => {:bar => "baz"})
#
# is the same as running <tt>assigns[:special_foo] = mock_model(Foo, :bar => "baz").
#
# <b>Note:</b> Adding to the assigns collection returns the object added, so this can
# be chained a la <tt>@foo = mock_and_assign(Foo)</tt>.
def mock_and_assign(klass, *args)
options = args.extract_options!
mocked = if options[:stub]
mock_model(klass, options[:stub])
else
mock_model(klass)
end
yield mocked if block_given?
self.assigns[args.first || "#{klass}".underscore] = mocked
end
# Creates an array of <tt>mock_model</tt> instances in the manner of
# <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!
return_me = Array.new(options[:size] || 3) do
mocked = if options[:stub]
mock_model(klass, options[:stub])
else
mock_model(klass)
end
yield mocked if block_given?
mocked
end
self.assigns[args.first || "#{klass}".tableize] = return_me
end
private
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
else
error_message = "Cannot determine template for render. "
error_message << "Please define @the_template in the before block "
error_message << "or name your describe block so that it indicates the correct template."
raise NameError, error_message
end
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
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ViewSpecHelpers for more information.
module ExampleGroupLevelMethods
include LuckySneaks::CommonSpecHelpers
# Creates an expectation which calls <tt>submit_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a Proc to evaluate the route not simply a named route
# helper, which would be undefined in the scope of the example block.
def it_should_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should submit to #{(hint || route)}" do
do_render
response.should 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.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to create the expectation for <tt>form_for</tt>
# 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)
it "should have a form_for(@#{name})" do
template.should_receive(:form_for).with(instance_for(name))
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)
it "should not have a form_for(@#{name})" do
template.should_not_receive(:form_for).with(instance_for(name))
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.
#
# <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)
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)
end
end
# Creates an expectation that the rendered template contains a <tt>FORM</tt> element
# (<tt>INPUT</tt>, <tt>TEXTAREA</tt>, or <tt>SELECT</tt>) with the specified name.
def it_should_have_form_element_for(name)
it "should have a form element named '#{name}'" do
do_render
response.should have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Negative version of <tt>it_should_have_form_element_for</tt>. See that method
# for more details.
def it_should_not_have_form_element_for(name)
it "should not have a form element named '#{name}'" do
do_render
response.should_not have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Creates an expectation which calls <tt>have_link_or_button_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a block to evaluate the route in the example context
# instead of the example group context.
def it_should_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_have_link_to it_should_link_to
alias it_should_have_button_to it_should_link_to
alias it_should_have_button_or_link_to it_should_link_to
# Negative version of <tt>it_should_link_to</tt>. See that method
# for more details.
def it_should_not_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should_not have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_not_have_link_to it_should_not_link_to
alias it_should_not_have_button_to it_should_not_link_to
alias it_should_not_have_button_or_link_to it_should_not_link_to
# Creates an expectation which calls <tt>have_link_or_button_to_new</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def it_should_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should have_link_or_button_to_new(name)
end
end
alias it_should_have_link_to_new it_should_link_to_new
alias it_should_have_button_to_new it_should_link_to_new
alias it_should_have_button_or_link_to_new it_should_link_to_new
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should_not have_link_or_button_to_new(name)
end
end
alias it_should_not_have_link_to_new it_should_not_link_to_new
alias it_should_not_have_button_to_new it_should_not_link_to_new
alias it_should_not_have_button_or_link_to_new it_should_not_link_to_new
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_show</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_have_link_to_show it_should_link_to_show
alias it_should_have_button_to_show it_should_link_to_show
alias it_should_have_button_or_link_to_show it_should_link_to_show
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should_not have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_not_have_link_to_show it_should_not_link_to_show
alias it_should_not_have_button_to_show it_should_not_link_to_show
alias it_should_not_have_button_or_link_to_show it_should_not_link_to_show
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_show_each(name)
it "should have a link/button to show each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_show(member)
end
end
end
alias it_should_have_link_to_show_each it_should_link_to_show_each
alias it_should_have_button_to_show_each it_should_link_to_show_each
alias it_should_have_button_or_link_to_show_each it_should_link_to_show_each
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_edit</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_have_link_to_edit it_should_link_to_edit
alias it_should_have_button_to_edit it_should_link_to_edit
alias it_should_have_button_or_link_to_edit it_should_link_to_edit
# Negative version of <tt>it_should_link_to_edit</tt>. See that method
# for more details.
def it_should_not_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should_not have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_not_have_link_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_or_link_to_edit it_should_not_link_to_edit
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_edit_each(name)
it "should have a link/button to edit each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_edit(member)
end
end
end
alias it_should_have_link_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_or_link_to_edit_each it_should_link_to_edit_each
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_delete</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_delete(name)
it "should have a link/button to delete @#{name}" do
do_render
response.should have_button_to_delete(instance_for(name))
end
end
alias it_should_have_link_to_delete it_should_link_to_delete
alias it_should_have_button_to_delete it_should_link_to_delete
alias it_should_have_button_or_link_to_delete it_should_link_to_delete
# Negative version of <tt>it_should_link_to_delete</tt>. See that method
# for more details.
def it_should_not_link_to_delete(name)
it "should not have a link/button to delete @#{name}" do
do_render
response.should_not have_button_to_delete(instance_for(name))
end
end
alias it_should_not_have_link_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_or_link_to_delete it_should_not_link_to_delete
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_delete_each(name)
it "should have a link/button to delete each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_button_to_delete(member)
end
end
end
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
end
end
end

View file

@ -0,0 +1,26 @@
# Let's make sure everyone else is loaded
require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment")
require 'spec'
require 'spec/rails'
begin
require 'ruby2ruby'
rescue
puts "-----"
puts "Attention: skinny_spec requires ruby2ruby for nicer route descriptions"
puts "It is highly recommended that you install it: sudo gem install ruby2ruby"
puts "-----"
end
# Let's load our family now
require "lucky_sneaks/common_spec_helpers"
require "lucky_sneaks/controller_request_helpers"
require "lucky_sneaks/controller_spec_helpers"
require "lucky_sneaks/controller_stub_helpers"
require "lucky_sneaks/model_spec_helpers"
require "lucky_sneaks/view_spec_helpers"
# Let's all come together
Spec::Rails::Example::ViewExampleGroup.send :include, LuckySneaks::ViewSpecHelpers
Spec::Rails::Example::HelperExampleGroup.send :include, LuckySneaks::CommonSpecHelpers
Spec::Rails::Example::ControllerExampleGroup.send :include, LuckySneaks::ControllerSpecHelpers
Spec::Rails::Example::ModelExampleGroup.send :include, LuckySneaks::ModelSpecHelpers