layout | title | excerpt | tags |
---|---|---|---|
post |
How I learned to test my Rails applications, Part 4: Controller specs |
Controllers are the glue in a Rails application, and should be tested just like any other code. This primer will guide you to better coverage in your own apps' controllers. |
rspec |
Poor controllers. As Rails developers we keep them skinny (which is a good thing) and often don't give them due attention in our tests (which is a bad thing; more on that in a moment). As you continue to improve your application's test coverage, though, controllers are the next logical chunk of code to tackle.
Following the lead of some prominent Ruby developers I stopped working on controller specs for awhile, in favor of covering this functionality in my request specs (integration tests). At the time I liked this idea a lot--using tests that more closely mirrored how controller actions are accessed made good sense--but since then I've come back to testing controllers more explicitly, for a couple of primary reasons:
- Controllers are models too, as Piotr Solnica indicated in an excellent blog post. And in Rails applications, they're pretty important models--so it's a good idea to put them on equal footing, spec-wise, as your Rails models.
- Controller specs can be written more quickly than their integration spec counterparts. For me, this becomes critical when I encounter a bug that's residing at the controller level, or I want to add additional specs to verify some refactoring. Writing a solid controller spec is a comparatively straightforward process, since I can generate very specific input to the method I'm testing without the overhead of request specs. This also means that
- Controller specs run more quickly than request specs, making them very valuable during bug fixing and checking the bad paths your users can take (in addition to the good ones, of course).
Scaffolds, when done correctly, are a great way to learn coding techniques. The spec files generated for controllers, at least as of RSpec 2.8, are pretty nice and provide a good template to help you build your own specs. Look at the scaffold generator in rspec-rails' source, or generate a scaffold in your properly configured Rails application to begin getting a sense of these tests. (Another generator to look at is the one in Nifty Generator's scaffolds).
A controller spec is broken down by controller method—each example is based off of a single action and, optionally, any params passed to it. Here's a simple example:
{% highlight ruby %} it "redirects to the home page upon save" do post :create, contact: Factory.attributes_for(:contact) response.should redirect_to root_url end {% endhighlight %}
If you've been following along with this series, you may notice similarities to earlier specs we've written:
- The description of the example is written in explicit, active language.
- The example only expects one thing: After the post request is processed, a redirect should be returned to the browser.
- A factory generates test data to pass to the controller method; note the use of Factory Girl's
attributes_for
option, which generates a hash of values as opposed to a Ruby object.
However, there are also a couple of new things to look at:
- The basic syntax of a controller spec—it's REST method (
post
), controller method (:create
), and, optionally, parameters being passed to the method. - The aforementioned
attributes_for
call to Factory Girl—not rocket science, but worth mentioning again because I had a habit early on of forgetting to use it versus default factories.
Let's start with a top-down approach. As I mentioned in the previous post on model specs, it's helpful to think about a spec as an outline. Continuing our use of an address book app as an example, here are some things I might need to test:
{% highlight ruby %}
require 'spec_helper'
describe ContactsController do describe "GET #index" do it "populates an array of contacts" it "renders the :index view" end
describe "GET #show" do it "assigns the requested contact to @contact" it "renders the :show template" end
describe "GET #new" do it "assigns a new Contact to @contact" it "renders the :new template" end
describe "POST #create" do context "with valid attributes" do it "saves the new contact in the database" it "redirects to the home page" end
context "with invalid attributes" do
it "does not save the new contact in the database"
it "re-renders the :new template"
end
end end {% endhighlight %}
And so on. As in our model specs, we can use RSpec's describe
and context
blocks to organize examples into a clean hierarchy, based on a controller's actions and the context we're testing—in this case, the happy path (a user passed valid attributes to the controller) and the unhappy path (a user passed invalid or incomplete attributes). If your application includes an authentication or authorization layer, you can include these as additional contexts—say, testing with and without a logged-in user, or testing based on a user's assigned role within the app.
With some organization in place, let's go over some things you might want to test in your application and how those tests would actually work.
Just as in model specs, controller specs need data. Here again we'll use factories to get started—once you've got the hang of it you can swap these out with more efficient means of creating test data, but for our purposes (and this small app) factories will work great.
We've already got a factory to generate a valid contact:
{% highlight ruby %}
factory :contact do |f| f.firstname { Faker::Name.first_name } f.lastname { Faker::Name.last_name } end {% endhighlight %}
Now let's add one to return an invalid contact:
{% highlight ruby %} factory :invalid_contact, parent: :contact do |f| f.firstname nil end {% endhighlight %}
Notice the subtle difference: The :invalid_contact
factory uses the :contact
factory as a parent. It replaces the specified attributes (in this case, firstname
with its own; everything else will defer to the original :contact
factory.
To date, the little address book app we've built is pretty basic. It doesn't even require a user to log in to view it or make changes. I'll revisit this in a future post; for now I want to focus on general controller testing practices.
A standard Rails controller is going to have four GET-based methods: #index
, #show
, #new
, and #edit
. Looking at the outline started above we can add the following tests:
{% highlight ruby %}
describe "GET #index" do it "populates an array of contacts" do contact = Factory(:contact) get :index assigns(:contacts).should eq([contact]) end
it "renders the :index view" do get :index response.should render_template :index end end
describe "GET #show" do it "assigns the requested contact to @contact" do contact = Factory(:contact) get :show, id: contact assigns(:contact).should eq(contact) end
it "renders the #show view" do get :show, id: Factory(:contact) response.should render_template :show end end {% endhighlight %}
Pretty simple stuff for the methods that, typically, have the lightest load to carry in controllers. In this address book app, the new
method is a little more complicated, though—it's building some new phones to be nested in the contact information form:
{% highlight ruby %}
def new @contact = Contact.new %w(home office mobile).each do |phone| @contact.phones.build(phone_type: phone) end end {% endhighlight %}
How to test iterating through those phone types? Maybe something like this:
{% highlight ruby %}
describe "GET #new" do it "assigns a home, office, and mobile phone to the new contact" do get :new assigns(:contact).phones.map{ |p| p.phone_type }.should eq %w(home office mobile) end end
{% endhighlight %}
The point here is if your controller methods are doing things besides what a generator might yield, be sure to test those additional steps, too.
Let's move on to our controller's :create
method. Referring back to our outline, we've got two contexts to test: When a user passes in attributes for a valid contact, and when an invalid contact is entered. The resulting examples look something like this:
{% highlight ruby %}
describe "POST create" do context "with valid attributes" do it "creates a new contact" do expect{ post :create, contact: Factory.attributes_for(:contact) }.to change(Contact,:count).by(1) end
it "redirects to the new contact" do
post :create, contact: Factory.attributes_for(:contact)
response.should redirect_to Contact.last
end
end
context "with invalid attributes" do it "does not save the new contact" do expect{ post :create, contact: Factory.attributes_for(:invalid_contact) }.to_not change(Contact,:count) end
it "re-renders the new method" do
post :create, contact: Factory.attributes_for(:invalid_contact)
response.should render_template :new
end
end end
{% endhighlight %}
Let's talk about that expect {}
Proc for a minute. RSpec's readability shines here—except this code to (or to not) do something. This one little example succinctly tests that an object is created and stored. (If Proc objects seem magical to you, refer to this post by Alan Skorkin and this one by Robert Sosinski to learn more.) Become familiar with this technique, as it'll be very useful in testing a variety of methods in controllers, models, and eventually at the integration level.
On to our controller's update
method, where we need to check on a couple of things—first, that the attributes passed into the method get assigned to the model we want to update; and second, that the redirect works as we want. Then we need to test that those things don't happen if invalid attributes are passed through the params:
{% highlight ruby %}
describe 'PUT update' do before :each do @contact = Factory(:contact, firstname: "Lawrence", lastname: "Smith") end
context "valid attributes" do
it "located the requested @contact" do
put :update, id: @contact, contact: Factory.attributes_for(:contact)
assigns(:contact).should eq(@contact)
end
it "changes @contact's attributes" do
put :update, id: @contact,
contact: Factory.attributes_for(:contact, firstname: "Larry", lastname: "Smith")
@contact.reload
@contact.firstname.should eq("Larry")
@contact.lastname.should eq("Smith")
end
it "redirects to the updated contact" do
put :update, id: @contact, contact: Factory.attributes_for(:contact)
response.should redirect_to @contact
end
end
context "invalid attributes" do
it "locates the requested @contact" do
put :update, id: @contact, contact: Factory.attributes_for(:invalid_contact)
assigns(:contact).should eq(@contact)
end
it "does not change @contact's attributes" do
put :update, id: @contact,
contact: Factory.attributes_for(:contact, firstname: "Larry", lastname: nil)
@contact.reload
@contact.firstname.should_not eq("Larry")
@contact.lastname.should eq("Smith")
end
it "re-renders the edit method" do
put :update, id: @contact, contact: Factory.attributes_for(:invalid_contact)
response.should render_template :edit
end
end end
{% endhighlight %}
The examples I want to point out here are the two that verify whether or not an object's attributes are actually changed by the update
method. Note that we have to call reload
on @contact
to check that our updates are actually persisted.
Testing destroy
is relatively straightforward:
{% highlight ruby %}
describe 'DELETE destroy' do before :each do @contact = Factory(:contact) end
it "deletes the contact" do
expect{
delete :destroy, id: @contact
}.to change(Contact,:count).by(-1)
end
it "redirects to contacts#index" do delete :destroy, id: @contact response.should redirect_to contacts_url end end
{% endhighlight %}
By now you should be able to correctly guess what everything's doing. The first expectation checks to see if the destroy
method in the controller actually deleted the object; the second expectation confirms that the user is redirected back to the index upon success.
Now that I've shared some of the many things you can test in your controllers, let me be honest with you—it wasn't until recently that I began testing at this level of my apps with such thoroughness. In fact, for a long time my controller specs just tested a few basics. But as you can see from RSpec's generated examples, there are several things you can—and should—test at the controller level.
And with thoroughly tested controllers, you're well on your way to thorough test coverage in your application as a whole. By now (between this post and the one on testing Rails models) you should be getting a handle on good practices and techniques for the practical use of RSpec, Factory Girl, and other helpers to make your tests and code more reliable.
In the MVC triumvarate, we've now covered the Model and Controller layers. Next time we'll integrate the two—along with view—with RSpec request specs. Thanks as always for reading, and let me know what you think in the comments.