Rails 3, SOAP and Testing, Oh My!

This past week at work I have had the “pleasure” of building out a SOAP endpoint for an internal system. This has caused me to find a wonderful new gem Wash Out (https://github.com/inossidabile/wash_out). With a feature comes with new test and with my first SOAP endpoint comes, how to test SOAP endpoints.

Testing a Wash Out controller wasn’t something that was blaintiantly obvious to me and took some experimenting and discussions with Boris Staal (@_inossidabile). Below is an example of how we settled on testing. This may not be the end all perfect solution but hopefully it will help you get started.

Let’s start with a sample controller, this will give us a base to refer to with our tests.

class API::GasPrices < ApplicationController
  include WashOut::SOAP
  soap_action "GetGasPrices", 
              :args   => {:postal_code => :integer}, 
              :return => :string
  def GetGasPrices
    render :soap => GasPrices.find_all_by_postal_code(params[:postal_code]).to_xml.to_s
  end
end

This controller is a fairly standard example, it has one method GetGasPrices and takes a postal_code as an argument. It returns a string of gas prices.

One of the things that we got caught on was how to actually hit the Wash Out gem and execute the HTTP request. To do that we’ll need to mount our app as a rack app.

We’ll need to make sure that we are using a version of the HTTPI gem that can use a Rack adapter. Right now we need to point our HTTPI gem at the GitHub repo. For the actual testing of making SOAP calls we can use Savon.

gem 'httpi', :git => 'https://github.com/savonrb/httpi.git'
gem 'savon'

Next we’ll need to create a spec file for our tests. For this example let’s use a request spec, even though this is a controller we actually want to make a request with SOAP to make sure our methods are recieving information correctly.

|~spec/
| |+requests/
| | |+API/
| | | |-gas_prices_controller_spec.rb

Let’s setup our spec file now, we’ll need to require Savon and the spec_helper.
Then create a describe block like below.

require 'spec_helper'
require 'savon'

describe API::GasPrices do
  HTTPI.adapter = :rack
  HTTPI::Adapter::Rack.mount 'application', MyGasPriceChecker::Application

  it 'can get gas prices for a postal code' do
    application_base = "http://application"
    client = Savon::Client.new({:wsdl => application_base + api_gas_prices_path })
      result = client.call(:get_gas_prices, :message =&gt; { :postal_code => 54703 })
    result.body[:get_gas_prices_response][:value].should_not be_nil
  end

Inside of our describe block we are using the HTTPI rack adapter, and then configuring that adapter to mount our MyGasPriceChecker as application. This will give us the ability to use the url http://application. In our test we’ll create a new Savon client, this client will need to access our WSDL so it can find the operations it has access too.

Once we have a client created our code can now actually call the GetGasPrices SOAP method. Our test then verifies that the value of our response is not nil, this is really just a starting point and we can iterate going forward to test actual return values.

Controller Testing

Recently I have gotten to work on a greenfield application, this has led to some discussions about the best way to test things. I personally have been taking time to write tests that allow me to take small steps giving me a better sense of direction. These small steps have allowed me to write a test, make it pass, write the next test, make it pass then refactor.

Continuously refactoring my code keeps it clean and maintainable. I’m not going for cleverness or golfing to the lowest number of lines of code. Instead I’m going for code that is flexible and allows me to continue to add new features with ease.

Below is an example of a controller test, written in two different styles. We’ll walk through a small refactoring scenario and see how we can keep our test simple and testing result rather than the implementation by comparing the two styles.

Disclaimer this example has some assumptions such as I am using FactoryGirl and Rspec.

Here is our starting controller, we are going to focus on the edit action. This is pretty strait forward, we’re going to edit an instance of ‘Foo’ so we’ll return it to the view.

class FooController > ApplicationController 
  def edit
   @foo = Foo.find(params[:id])
  end
end

One way we can test this is to build an object with FactoryGirl and then stub out the find method on Foo to return that object. This will allow us to test our edit action insuring that foo is always the same thing.

describe "GET 'edit'" do
  it 'should receive the STUBBED Foo instance' do
    @foo = FactoryGirl.build(:foo)
    Foo.stub(:find).and_return(@foo)
    get :edit, :id => @foo.id
  
    expect(assigns(:foo)).to eql @foo
  end
end

Another way we could test this trivial example is to just use FactoryGirl to create the object rather than just build it. This takes a little less code, but does require two hits to the database, one for saving the record and one for retrieving.

The benefit of this is we are setting up that if we send in the ‘id’ of ‘@foo’ we’ll get back an identical ‘@foo’ from the database.

Our controller test now is just saying hey we expect to get ‘@foo’ back, we really don’t care how you get it but in order for this edit form to work we need this ‘@foo’ back.

describe "GET 'edit'" do
  it 'should receive the NON stubbed Foo instance' do
    @foo = FactoryGirl.create(:foo)
    get :edit, :id => @foo.id
  
    expect(assigns(:foo)).to eql @foo
  end
end

Now let’s refactor our Foo controller to do something a little different. Now we decided that really the ‘@foo’ should be retrieved by sending the ‘@foo.id’ to a method called ‘by_bar’

class FooController < ApplicationController 
  def edit
     @foo = Foo.by_bar(params[:id])
  end
end

Here is our new class method that replaces the normal Foo.find.

class Foo
  def self.by_bar(id)
    Bar.find_by_foo_id(id).foo
  end
end

Now in our first version of the test we are going to get an error because we are not actually creating and saving the object and it is only stubbing out the find method making it brittle and tied to the implementation.

Let’s refactor our tests, first we’ll start with the test using a stub, we’ll need to modify the stub to use the ‘by_bar’ method.

Next we’ll look at the test not using a stub, this one does not need any work. Again we are testing to make sure that the instance of ‘Foo’ we are expecting is returned. In this case it is, so we don’t need to do anything.

describe "GET 'edit'" do
  it 'should receive the STUBBED Foo instance' do
    @foo = FactoryGirl.build(:foo)
    Foo.stub(:by_bar).and_return(@foo)
    get :edit, :id => @foo.id
  
    expect(assigns(:foo)).to eql @foo
  end
  
  it 'should receive the NON stubbed Foo instance' do
    @foo = FactoryGirl.create(:foo)
    get :edit, :id => @foo.id
  
    expect(assigns(:foo)).to eql @foo
  end
end

While looking at our refactoring we realize that we didn’t really need a separate method for our ‘Bar.find_by_foo’ and we can just move that into our controller like so.

class FooController < ApplicationController 
  def edit
     @foo = Bar.find_by_foo_id(id).foo
  end
end

Now our stubbed test breaks again, let’s see what it will take to fix it. We’ll have to change our stub again. This time the stub needs to be done on the ‘Bar’ class and ‘find_by_foo_id’ method. Again our non-stubbed test continues to work because we are still returning an instance of ‘Foo’

describe "GET 'edit'" do
  it 'should receive the STUBBED Foo instance' do
    @foo = FactoryGirl.build(:foo)
    Bar.stub(:find_by_foo_id).and_return(@foo)
    get :edit, :id => @foo.id
  
    expect(assigns(:foo)).to eql @foo
  end
  
  it 'should receive the NON stubbed Foo instance' do
    @foo = FactoryGirl.create(:foo)
    get :edit, :id => @foo.id
  
    expect(assigns(:foo)).to eql @foo
  end
end

As we have seen here taking small steps and limiting our stubs and mocks will save us time when refactoring. In all of these examples the things that broke the test were just breaking test setup code rather than actually failing the test giving us a false positive.