Showing posts with label ruby on rails. Show all posts
Showing posts with label ruby on rails. Show all posts

Monday, August 11, 2008

Using rspec to test actionwebservice with Rails 2

Background:
I'm currently working on an application for a client who requested that web services being served by the app are in WSDL format. In Ruby on Rails (before REST became the de facto standard), this was accomplished by using Actionwebservice, but unfortunately it was removed from the Rails core as of version 2. Luckily, it's still available as a gem, and can be reintegrated with your RoR application by following these instructions: Using Action Web Service with rails 2.0

Problem:
After I got AWS working with my application, the problem of testing these methods arose. Even before AWS was ousted from the Rails core, documentation on AWS testing was sparse and any available material usually ended up referring to this RDOC. The RDOC indicates that testing AWS methods should be similar to functional tests for controllers.

While this may have worked, I wanted to test my web service methods using rspec and Mocha for mocking the model behaviour (instead of having to use fixtures), and also to decouple the controller tests from the model layer.

Solution :)
I'm assuming at this point that the rspec testing framework is working properly with your RoR application, and that you're familiar on how to generate regular controller tests with it.

As a side note, I'm using Mocha for mocking data, which can be installed with "gem install mocha" and adding "config.mock_with :mocha" in your spec_helper.rb file.

Finally, to enable rspec tests to work with your AWS controller, add the following line to the top of your *_controller_spec.rb file:
require 'action_web_service/test_invoke'


With these preparations completed, we can move onto the actual tests. As an example, I had the following authentication method to test:

def authenticate(username, password)
# Authenticate user
@user = User.try_to_login(username, password)

if @user
return @user
else
return nil
end
end

(I removed some code in this method for the purposes of this example, so I'm aware that this could have been done in one line without the need for the if statement)

Where User.try_to_login would return a User object if the login was valid, or nil otherwise. Now in order to test this, we need to make sure that the model logic is clearly separate from the controller logic. This means that User.try_to_login should not actually hit the User model, and instead be mocked in the test.

The following is my rspec test for this method:

describe SysController, "The 'authenticate' method" do

before(:each) do
@user = User.new
end

it "should return a User object for valid users" do
User.stubs(:try_to_login).returns(@user)

result = invoke :authenticate, "valid", "valid"
result.should_not be_nil
result.should be_an_instance_of(User)
end

it "should return nil for invalid users" do
User.stubs(:try_to_login).returns(nil)

result = invoke :authenticate, "invalid", "invalid"
result.should be_nil
end

end


The line "User.stubs(:try_to_login).returns(@user)" is where Mocha works it magic. This line basically intercepts any call for the method "try_to_login" to the User object and modifies its return, all without being affected by any model logic.

Another thing to note is that the actual arguments sent to the authenticate method ("valid", "valid" or "invalid","invalid") in both tests shouldn't have any bearing on the controller logic it's testing since these arguments should only affect the model logic for try_to_login. Instead, the controller is only concerned with the return of the try_to_login method, i.e. whether @user is returned as nil or a User object.

Although this probably wasn't the best example, it can still give you a general idea on how to implement rspec testing with Actionwebservice controllers and using Mocha to mock model behaviour.

Thursday, December 28, 2006

has_many :through associations and the push (<<) operator.

Background:
I needed to create a many-to-many association between two of my models, called Band and Show, i.e. a band can be a part of many shows, and many shows have bands performing. I considered the traditional method of using a has_and_belongs_to_many (habtm) association and creating a "bands_shows" join table. However, the associations themselves needed to contain their own set of information. This is, of course, still possible with a habtm association. Unfortunately, it's more of a hack than an elegant solution.

This is where has_many :through associations come in. has_many :through is a method for creating a many-to-many association between two models and contain a set of information within themselves. There is sufficient documentation of it through Google already, so I won't go over all the details. For a comparison between habtm and has_many through association, visit http://weblog.rubyonrails.org/2006/04/21/habtm-vs-has_many-through/

Problem:
For my project, I mentioned above that the two models I was trying to associate were Show and Band. I decided to call their associating join table "Performance," since a band's relationship to a show is called a performance. I set up my database and Rails models, and everything was going great... until I tried to use the push operator, e.g.
Show.find(:first).bands << Band.find(:first)
This is where my world came crashing down. Since, at this point, I've only used habtm associations, I was expecting to be able to add these relationships just as easily as using a push operator. Oh, how wrong was I. A Google search then revealed to me that the push operator does not work with has_many :through associations. SADFACE, I thought. Will I have to resort to creating a method in the Show model called add_band and a method in the Band model called add_show? No, that's ugly. I refuse.

Solution :)
A little more research then revealed that a push operator solution was already in the works and was implemented in Rails edge. Unfortunately, I couldn't (or didn't want to) upgrade to edge, so I decided to implement my own push method and remove it when Rails stable implemented it, thereby not having to edit any of the method calls.

Band.rb:

class Band < ActiveRecord::Base
has_many :shows, :through => :performances, do
def <<(show)
begin
Performance.create!(:show => show, :band => @owner)
self.concat([show])
rescue
nil
end
end
end
end

Pretty simple, isn't it? What's important to note is @owner. Rails is a little wonky with the way it handles closures, so when you're in the has_many :shows block, "self" actually references the association array, rather than the Band object. In any case, there it is. Here are the other two models, just in case you need them.

Show.rb:

class Show < ActiveRecord::Base
has_many :bands, :through => :performances do
def <<(band)
begin
Performance.create!(:show => @owner, :band => band)
self.concat([band])
rescue
nil
end
end
end
end


Performance.rb:

class Performance < ActiveRecord::Base
belongs_to :band
belongs_to :show
end