Skip to content
Browse files

initial commit

  • Loading branch information...
0 parents commit 650671cd5ffe056f2c3c5a680559421e2dff246d rsl committed May 14, 2008
Showing with 1,801 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +129 −0 README.rdoc
  3. +11 −0 Rakefile
  4. +58 −0 additional/helper_overrides.txt
  5. +92 −0 generators/skinny_scaffold/skinny_scaffold_generator.rb
  6. +10 −0 generators/skinny_scaffold/templates/controller.rb
  7. +103 −0 generators/skinny_scaffold/templates/controller_spec.rb
  8. +18 −0 generators/skinny_scaffold/templates/form.html.haml
  9. +44 −0 generators/skinny_scaffold/templates/form.html.haml_spec.rb
  10. +2 −0 generators/skinny_scaffold/templates/helper.rb
  11. +5 −0 generators/skinny_scaffold/templates/helper_spec.rb
  12. +22 −0 generators/skinny_scaffold/templates/index.html.haml
  13. +15 −0 generators/skinny_scaffold/templates/index.html.haml_spec.rb
  14. +11 −0 generators/skinny_scaffold/templates/index_partial.html.haml
  15. +31 −0 generators/skinny_scaffold/templates/index_partial.html.haml_spec.rb
  16. +14 −0 generators/skinny_scaffold/templates/migration.rb
  17. +2 −0 generators/skinny_scaffold/templates/model.rb
  18. +20 −0 generators/skinny_scaffold/templates/model_spec.rb
  19. +13 −0 generators/skinny_scaffold/templates/show.html.haml
  20. +32 −0 generators/skinny_scaffold/templates/show.html.haml_spec.rb
  21. +3 −0 init.rb
  22. +25 −0 lib/lucky_sneaks/common_spec_helpers.rb
  23. +66 −0 lib/lucky_sneaks/controller_request_helpers.rb
  24. +289 −0 lib/lucky_sneaks/controller_spec_helpers.rb
  25. +196 −0 lib/lucky_sneaks/controller_stub_helpers.rb
  26. +222 −0 lib/lucky_sneaks/model_spec_helpers.rb
  27. +340 −0 lib/lucky_sneaks/view_spec_helpers.rb
  28. +26 −0 lib/skinny_rspec.rb
2 .gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+doc
129 README.rdoc
@@ -0,0 +1,129 @@
+= 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.
+To benefit from Skinny Spec's scaffolding, you'll need to be using make_resourceful as well.
+I've got plans to add a more generic Rails' scaffolding but I have <i>lots</i> of plans so
+don't hold your breath.
+
+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 Foo
+
+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. 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.
+
+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. 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 :foos
+
+This wraps an expectation that <tt>Foo.should_receive(:find).with(:all)</tt>. 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.) You can add
+more detailed arguments to that find. Check the documentation for <tt>it_should_find</tt> for
+more information. You might have guessed that <tt>it_should_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.
+
+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! 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_link_to_<i>controller_method</i></tt>. These methods (there's one each for
+<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>. Second 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
+
+There's not a lot that Skinny Spec does here besides add a matcher for the various ActiveRecord
+associations. On the example group level you call them like:
+
+ it_should_belong_to :foo
+ it_should_have_many :bars
+
+Within an example you can call them on either the class or the instance setup in the
+"before" block. These are equivalent:
+
+ @foo.should belong_to(:bar)
+ Foo.should belong_to(:bar)
+
+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].
11 Rakefile
@@ -0,0 +1,11 @@
+require 'rake'
+require 'rake/rdoctask'
+
+desc 'Generate documentation for the Skinny Rspec plugin'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = 'Skinny Rspec'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
58 additional/helper_overrides.txt
@@ -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
92 generators/skinny_scaffold/skinny_scaffold_generator.rb
@@ -0,0 +1,92 @@
+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
+ 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
+ %w{index show form}.each do |action|
+ m.template "#{action}.html.haml",
+ File.join('app/views', controller_class_path, controller_file_name, "#{action}.html.haml")
+ m.template "#{action}.html.haml_spec.rb",
+ File.join('spec/views', controller_class_path, controller_file_name, "#{action}.html.haml_spec.rb")
+ end
+ m.template 'index_partial.html.haml',
+ File.join('app/views', controller_class_path, controller_file_name, "_#{file_name}.html.haml")
+ m.template 'index_partial.html.haml_spec.rb',
+ File.join('spec/views', controller_class_path, controller_file_name, "_#{file_name}.html.haml_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
10 generators/skinny_scaffold/templates/controller.rb
@@ -0,0 +1,10 @@
+class <%= controller_class_name %>Controller < ApplicationController
+ make_resourceful do
+ actions :all
+
+ # Let's get the most use from form_for and share a single form here!
+ response_for :new, :create_fails, :edit, :update_fails do
+ render :template => "<%= plural_name %>/form"
+ end
+ end
+end
103 generators/skinny_scaffold/templates/controller_spec.rb
@@ -0,0 +1,103 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe <%= controller_class_name %>Controller do
+ def valid_attributes(args = {})
+ {
+ # Add valid attributes for the your params[:<%= singular_name %>] here!
+ }.merge(args)
+ end
+
+ describe "GET :index" do
+ before(:each) do
+ @<%= plural_name %> = stub_index(<%= class_name %>)
+ end
+
+ it_should_find :<%= plural_name %>
+ it_should_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 :<%= singular_name %>
+ it_should_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 :<%= singular_name %>
+ it_should_save :<%= singular_name %>
+ 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 :<%= singular_name %>
+ it_should_assign :<%= singular_name %>
+ it_should_render :template, "form"
+ end
+ end
+
+ describe "GET :show" do
+ before(:each) do
+ @<%= singular_name %> = stub_show(<%= class_name %>)
+ end
+
+ it_should_find :<%= singular_name %>
+ it_should_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 :<%= singular_name %>
+ it_should_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 :<%= singular_name %>
+ it_should_update :<%= singular_name %>
+ 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 :<%= singular_name %>
+ it_should_assign :<%= singular_name %>
+ it_should_render :template, "form"
+ end
+ end
+
+ describe "DELETE :destroy" do
+ before(:each) do
+ @<%= singular_name %> = stub_destroy(<%= class_name %>)
+ end
+
+ it_should_find :<%= singular_name %>
+ it_should_destroy :<%= singular_name %>
+ it_should_redirect_to { <%= plural_name %>_url }
+ end
+end
18 generators/skinny_scaffold/templates/form.html.haml
@@ -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"
44 generators/skinny_scaffold/templates/form.html.haml_spec.rb
@@ -0,0 +1,44 @@
+require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
+
+describe "<%= File.join(controller_class_path, controller_singular_name) %>/form.html.haml" 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
+
+ it "should use form_for to generate the proper form action and options" do
+ template.should_receive(:form_for).with(@<%= singular_name %>)
+ do_render
+ end
+
+<% 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
2 generators/skinny_scaffold/templates/helper.rb
@@ -0,0 +1,2 @@
+module <%= controller_class_name %>Helper
+end
5 generators/skinny_scaffold/templates/helper_spec.rb
@@ -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
22 generators/skinny_scaffold/templates/index.html.haml
@@ -0,0 +1,22 @@
+%h1 <%= model_name %> List
+%table
+ - if @<%= plural_name %>.empty?
+ %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
15 generators/skinny_scaffold/templates/index.html.haml_spec.rb
@@ -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.haml" 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
11 generators/skinny_scaffold/templates/index_partial.html.haml
@@ -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
31 generators/skinny_scaffold/templates/index_partial.html.haml_spec.rb
@@ -0,0 +1,31 @@
+require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
+
+describe "<%= File.join(controller_class_path, controller_singular_name) %>/_<%= singular_name %>.html.haml" 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 -%>
+ 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
14 generators/skinny_scaffold/templates/migration.rb
@@ -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
2 generators/skinny_scaffold/templates/model.rb
@@ -0,0 +1,2 @@
+class <%= class_name %> < ActiveRecord::Base
+end
20 generators/skinny_scaffold/templates/model_spec.rb
@@ -0,0 +1,20 @@
+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
+
+ # Add your model specs here, please!
+ # And don't forget about the association matchers built-in to Skinny Rspec like:
+ #
+ # it_should_have_many :foos
+ #
+ # Check out the docs for more information.
+end
13 generators/skinny_scaffold/templates/show.html.haml
@@ -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
32 generators/skinny_scaffold/templates/show.html.haml_spec.rb
@@ -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.haml" 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 init.rb
@@ -0,0 +1,3 @@
+if RAILS_ENV == "test"
+ require "skinny_rspec"
+end
25 lib/lucky_sneaks/common_spec_helpers.rb
@@ -0,0 +1,25 @@
+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.classify.constantize
+ end
+
+ # Returns instance variable for the specified name. Example:
+ #
+ # instance_for("foo") # => @foo
+ def instance_for(name)
+ instance_variable_get("@#{name}")
+ 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
+ end
+end
66 lib/lucky_sneaks/controller_request_helpers.rb
@@ -0,0 +1,66 @@
+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
+
+ 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 set define_request or 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
289 lib/lucky_sneaks/controller_spec_helpers.rb
@@ -0,0 +1,289 @@
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require "skinny_rspec"
+
+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
+
+ 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
+
+ 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
+
+ def it_should_save(name)
+ it "should save the #{name}" do
+ create_positive_ar_instance_expectation name, :save
+ eval_request
+ end
+ end
+
+ 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
+
+ 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
+
+ # Creates an expectation that the specified collection (<tt>flash</tt> or session)
+ # contains the specified key and value
+ def it_should_set(collection, key, value = nil, &block)
+ it "should set #{collection}[:#{key}]" do
+ eval_request
+ if value
+ self.send(collection)[key].should == value
+ 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>
+ def it_should_set_flash(name, value = nil)
+ it_should_set :flash, name, value
+ end
+
+ # Wraps <tt>it_should_set :session</tt>
+ def it_should_set_session(name, value = nil)
+ it_should_set :session, name, value
+ end
+
+ # Wraps the various <tt>it_should_render_<i>foo</i></tt> methods:
+ # <tt>it_should_render_template</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 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
196 lib/lucky_sneaks/controller_stub_helpers.rb
@@ -0,0 +1,196 @@
+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, :new_record? => true, :id => nil) 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)
+ 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.downcase.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
222 lib/lucky_sneaks/model_spec_helpers.rb
@@ -0,0 +1,222 @@
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require "skinny_rspec"
+
+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
+ 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
+ class_for(self.class.description_text) || 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
+ end
+ end
+end
340 lib/lucky_sneaks/view_spec_helpers.rb
@@ -0,0 +1,340 @@
+$:.unshift File.join(File.dirname(__FILE__), "..")
+require "skinny_rspec"
+
+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}]'],
+ 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
+ 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 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
+
+ # 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
+
+ # 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
+
+ # 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
+
+ # 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
+
+ # 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
+
+ # 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
+
+ def it_should_render(hint = nil, &block)
+ if hint.nil? && block.respond_to?(:to_ruby)
+ hint = block.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
+ end
+ it "should render #{hint || route}" do
+ template.expect_render &block
+ do_render
+ end
+ end
+ end
+ end
+end
26 lib/skinny_rspec.rb
@@ -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_rspec 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

0 comments on commit 650671c

Please sign in to comment.
Something went wrong with that request. Please try again.