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.
Obviously you’ll need to be using RSpec and Rspec-Rails as your testing framework.
Skinny Spec was originally designed [and best enjoyed] if you’re using Haml and make_resourceful 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 is highly recommended.
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!
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 script/generate scaffold
. Then have a look at the generated files (particularly the specs) to see what’s new and different with Skinny Spec.
Let’s look at the controller specs.
describe UsersController do def valid_attributes(args = {}) { # Add valid attributes for the your params[:user] here! }.merge(args) end describe "GET :index" do before(:each) do @users = stub_index(User) 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 valid_attributes
. This will be used later by the create
and update
specs to more accurately represent how the controller works in actual practice by supplying somewhat real data for the params
coming from the HTML forms.
Next we find an example group for GET :index
. That stub_index
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 stub_new
) but I will point out that the methods named stub_controller_method
should only be used for stubbing and mocking the main object of the method. To create mocks for other ancillary objects, please use stub_find_all
, stub_find_one
, and stub_initialize
. 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 define_request
in those example groups to define an explicit request, like so:
describe "PUT :demote" do define_request { put :demote } # ... end
You can also define a method called shared_request
to “share” a define_request
across nested 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 explicit requests by calling do_request
in your spec as in the following example:
# Note this controller is UsersController and _not_ CategoriesController # and that loading the categories isn't part of the default actions # and cannot use the <tt>stub_<i>controller_method</i></tt> helpers # [which create implicit requests based on the controller method in the name] # but uses <tt>stub_find_all</tt> instead describe "GET :new" do before(:each) do @user = stub_new(User) @categories = stub_find_all(Category) end # ... it "should preload categories" do Category.should_receive(:find).with(:all) do_request 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 User.should_receive(:find).with(:all)
and another that the instance variable @users
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 it_should_find
for more information. You might have guessed that it_should_initialize_assign
and it_should_render_template
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 with_default_restful_actions
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 it_should_redirect_to
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.
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 stub_index
methods in the controller specs, the view specs have a shorthand mock and stub helpers: mock_and_assign
and mock_and_assign_collection
. 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 it_should_have_form_for
. 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 it_should_allow_editing
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 name
attribute set in such away that it will generate the proper params
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 it_should_have_form_element_for
which is roughly equivalent for those times when you use form_tag
instead.
Finally let’s look at those it_should_link_to_controller_method
helpers. These methods (and there’s one each for the controller methods new
, edit
, show
, and delete
) point to instance variables which you should be created in the “before” blocks with mock_and_assign
. The other is it_should_allow_editing
which is likewise covered extensively in the documentation and I will just point out here that, like it_should_link_to_edit
and such, it takes a symbol for the name of the instance variable it refers to and additionally 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 render @that_template
you can simply call do_render
which takes the name of the template from the outermost example group where it is customarily stated.
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 it_should_validate_presence_of
, it_should_validate_uniqueness_of
, it_should_not_mass_assign
. Please consult the documentation for more information.
In the scaffolding, I have used my own idiomatic Rails usage:
-
All controller actions which use HTML forms [
new
,edit
, etc] use a shared form and leverageform_for
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
BUTTON
elements which are much easier to style than “button” typedINPUT
. 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 thelabel
method which uses#titleize
instead ofhumanize
for stylistic reasons). -
Probably more that I can’t think of.
Sections of this code were taken from or inspired by Rick Olsen’s rspec_on_rails_on_crack. 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.