Wednesday, February 15, 2012

Rails 3.1 testing with RSpec, Paperclip, and Mongoid

Coming in to this, I did not know much about how RSpec testing worked. I watched a couple of RailsCasts (here and here) to obtain a quick primer on the subject, but I still felt like it was not enough. So I decided to write this blog post in hopes that it will help anyone else who has had the same troubles as me. After watching the RailsCasts, the first thing I did was set up my test environment by installing the RSpec, FactoryGirl, and Guard gems. I will be writing tests for a model called Event and here is the basic structure of it:
class Event
   include Mongoid::Document
   include Mongoid::Paperclip

   field :name, type: String
   has_mongoid_attached_file :banner_image,
                 :storage => :s3,               
                 :s3_credentials => "#{Rails.root}/config/amazon_s3.yml",                
                 :s3_permissions =>'public-read',                
                 :s3_protocol => 'http',               
                 :s3_headers => {'Expires' => 1.year.from_now.httpdate},                 
                 :url => ':s3_alias_url'

   validates_presence_of :name, allow_nil: false
   validates_length_of :name, minimum: 1, maximum: 50, allow_blank: false
   validates_presence_of :start_time, allow_nil: false
end
So if you installed RSpec before creating this Event model, it should invoke an RSpec generator to automatically create the file Rails.root/spec/models/event_spec.rb, but if you installed RSpec after the fact, then just create the file manually.
require 'spec_helper'

describe Event do

end
So what are the most basic kinds of things that we can test in the model to start off? The easiest things I have found to test first are the model validations. The RSpec syntax is meant to be very human readable as you will see. The first validation to test is the mandatory presence of the name field. First we want to create a mock Event object to test with; we can do that by using FactoryGirl. To set up FactoryGirl, create a factories.rb file in the Rails.root/spec folder. This is where we will define what mock Event objects will look like, like so:
FactoryGirl.define do
   factory :event do
      name 'TestEvent'
      start_time { Time.now }
   end
end
Basically all our mock objects will have the name 'TestEvent' and a start_time of right now. There are many more advanced options and features in FactoryGirl that are available (here), but for now let's keep things simple.

Now back in the event_spec.rb file, I'll create a test called "should require a name", which will test that our validates_presence_of :name is working correctly. First, we need to use FactoryGirl to create a mock Event object. We can do this by calling FactoryGirl.build(:event). Notice that I am using the build method (creates an object, but does not save it to the database), rather than the create method (creates an object, and does save it to the database). We actually want to test that an Event object without a name should not be valid. So to create a mock Event with no name, we can actually set the Event's name by passing it in like so: FactoryGirl.build(:event, :name => nil). Putting this all together we get:
describe Event do
   it "should require a name" do
      event = FactoryGirl.build(:event, :name => nil)
      event.should_not be_valid
   end
end
The rest of the validation tests are similar and straight forward. I want to move on to the controller RSpec tests next. If this file is not already created, you should create it here Rails.root/spec/controllers/events_controller_spec.rb. Initially I had a lot of trouble trying to figure out how to write tests for the controller, but I came across a book called Rails Test Prescriptions: Keeping Your Application Healthy that was quite helpful. Now I didn't buy or read this book, but I downloaded the free sample code from the book here, and it contains some great examples of how to write simple tests for your basic CRUD controller methods. If you're interested in looking at the code, it is located in huddle3_rspec2/spec/controllers/projects_controller_spec.rb.

Let's start with a simple test of making a GET request on the index method of the EventsController. Just like in the model tests, we want to create some mock Event objects by calling FactoryGirl.create_list(:event, 10), which will create 10 mock Event objects. Now when we perform a get :index in the test, it's going to run the code that we have in the controller. Looking at the index method, we can see that in order to get all the events, it calls @events = Event.all, which is great for regular use, but not so much for testing. During testing, we would like to have a controlled environment where we can control the data being returned. We can accomplish this by something called method stubbing. Method stubbing allows you to intercept method calls within a function (so the method in question will not actually be called), and allow you to return whatever data you want. Luckily, this is very easy to do in RSpec. Here is the code:
describe EventsController do
   describe "GET index" do
      it "should assign all events as @events" do
         events = FactoryGirl.create_list(:event, 10)
         Event.stub(:all).and_return(events)

         get :index

         assigns(:events).should eq(events)
      end
   end
end
Let's walk through this code. First we create 10 events that we are going to make the Event.all method return. Then we actually stub the :all method so that it returns the events we created. Now in order to invoke the controller code, we need to send a GET request to the index method, which is what get :index does. Finally, we need to check to make sure that the @events variable (in the index method in the controller) gets correctly set to what we expect it to be, which are the Events we created with FactoryGirl. The assigns() method in RSpec allows you to access the instance variables (variables with an @ in front of it) of the controller. This test should pass because we stub out the :all method and therefore the @events variable gets set to the 10 events that we created in the beginning.

Now that we've had a crash course in RSpec, we can write a test to test if file attachments are working correctly with Paperclip. Before we start, I need to mention that Paperclip comes with RSpec matchers, which allow you test Paperclip specific attributes such as should have_attached_file(:file). This is great, but when using Mongoid, we have to use the mongoid-paperclip gem, which does not support these matchers. I did some research on Google and Stackoverflow, but I could not find a clear cut answer. So what I decided to do was just test if the object had the Paperclip fields defined and were set. When you define a Paperclip attachment in a model, it will create several fields for you. For example, if my Paperclip attachment is called :banner_image, then it will create the following fields in your model automatically: :banner_image_file_name, :banner_image_content_type, :banner_image_file_size, and :banner_image_updated_at; I wanted to test to make sure that these fields were not nil. Now you can just put these checks directly in to your test, but I wanted to be able to reuse these file checks without copying and pasting code. We can do this with something called RSpec Matchers, which allow you to define conditions that get tested after the should method. I won't go in to too much detail about them here, but you can watch this Railscasts for more information. I created a file called banner_image_matcher.rb in Rails.root/spec/support/matchers and put this inside:
# An RSpec matcher used to test the banner_image file attachment of the Event model. It tests whether the Paperclip fields for file attachments exist or not.
RSpec::Matchers.define :have_banner_image_attached_file do |expected|
   match do |actual|
      actual.banner_image_file_name != nil &&
      actual.banner_image_content_type != nil &&
      actual.banner_image_file_size != nil &&
      actual.banner_image_updated_at != nil
   end

   failure_message_for_should do |actual|
      "expected that #{actual} would have a banner_image file attachment"
   end

   failure_message_for_should_not do |actual|
      "expected that #{actual} would not have a banner_image file attachment"
   end
end
More detailed information can be found here, but basically inside the match block, you want to test your condition, and then return true or false. You can also modify the error messages that get printed when a test fails.

Now we can finally write the test. Here is what I did:
it "should assign a newly created event as @event with an image attachment" do 
   event = FactoryGirl.create(:event, :banner_image => File.new(Rails.root + 'spec/support/images/bannerTest.png'))
   Event.stub(:new).and_return(event)

   post :create, :event => event

   assigns(:event).should eq(event)
   assigns(:event).should have_banner_image_attached_file
end
Basically I do the same thing as my previous tests, but I add an extra check for the banner image using the RSpec matcher I just created. Also, I want to mention that you can provide a file to test with by reading one in using File.new. I put a test image in the Rails.root/spec/support/images folder and reference it there. This all works great, but there's on thing we're missing. If you're using Amazon S3 to upload your files to, these tests will actually upload your image every time you run your test. This is something we probably don't want to do, so we can prevent that from happening by stubbing out the method that uploads the file. That method is called save_attached_files, and we can stub it out by using the following code: Event.any_instance.stub(:save_attached_files).and_return(true). So now our test should look like this:
it "should assign a newly created event as @event with an image attachment" do 
   # stub out this method so that this test does not actually upload the image to amazon s3
   Event.any_instance.stub(:save_attached_files).and_return(true)

   event = FactoryGirl.create(:event, :banner_image => File.new(Rails.root + 'spec/support/images/bannerTest.png'))
   Event.stub(:new).and_return(event)

   post :create, :event => event

   assigns(:event).should eq(event)
   assigns(:event).should have_banner_image_attached_file
end
We can run this test (or have Guard run it for us) and see that it passes.

I hope that this post was helpful in getting started with RSpec testing, and also how to test with Paperclip and Mongoid. Feel free to ask questions in the comments and I will do by best to answer all of them.

No comments:

Post a Comment