Blog


Jun

Relieving the Pain of Controller Tests


Lately we've been embracing Cucumber as the preferred way of testing our Ruby on Rails applications. Cucumber is awesome, both for communicating with the customer and for getting thorough, full-stack tests of the application. We like Cucumber so much, we basically thought that it could replace both view and controller tests. It turns out we were wrong.

While our policy of Cucumber over view tests has been working out great so far, controllers are a different story. There is simply too much logic in the controller that is very hard to test (in a sane way) with Cucumber. It makes sense to have a cucumber feature that specifies that, for a non-admin user, a certain link should not be there, however that doesn't test the security of the application, despite the link not being there, the action may still be freely accessible for the user. Cucumber is not well suited (nor is it intended) to test these kinds of things.

But writing controller tests is a serious pain, so we tried to find a stack that felt natural and pleasant to work with. After some experimentation, we've settled on a slightly odd and interesting stack, consisting of the following:

  • Remarkable's descriptions and steps
  • RSpec's normal mocking syntax
  • Macro-style methods for different user contexts

We first tried using Remarkable on its own, but quickly found that we did not like the mocking syntax:

mock_models :data_point

describe(:post => :create, :data => "params") do
  expects :bulk_create, :on => DataPoint, 
          :with => proc { [@current_account, "params"] }, 
          :returns => proc { [mock_data_point] }

  it { should set_the_flash(:notice) }
  it { should render_template('data_points/new')}
  it { should assign_to(:data_points, :with => [mock_data_point]) }
end

The fact that it uses a "class-method" level for the DSL presents a lot of problems, it is impossible to simply use instance variables, methods need to be wrapped in procs, etc... It also, for some reason, does not seem to support stubs, which is very inconvenient in some cases. In the end we realized that there is absolutely no advantage to Remarkable's DSL over simply doing:

mock_models :data_point

describe(:post => :create, :data => "params") do
  before do
    DataPoint.should_receive(:bulk_create).with(@current_account, "params").and_return([mock_data_point])
  end

  it { should set_the_flash(:notice) }
  it { should render_template('data_points/new')}
  it { should assign_to(:data_points, :with => [mock_data_point]) }
end

One sore point though was that there was a lot of setup required in each controller spec for getting the logged in user right. We thought that with some block trickery we might be able to take care of this tedious setup:

module LogInContext

  def as_user(params={}, &block)
    describe "(as a logged in user)" do
      before do
        @current_user = mock('current_user')
        controller.stub!(:current_user).and_return(@current_user)
      end

      describe(params, &block)
    end
  end

  ...

  def deny_access_to_visitors(params={})
    as_visitor(params) do
      it { should redirect_to(new_session_path) }
    end
  end

end

Spec::Rails::Example::ControllerExampleGroup.extend(LogInContext)

Now we can use these contexts in our controller tests:

mock_models :data_point

as_user(:post => :create, :data => "params") do
  before do
    DataPoint.should_receive(:bulk_create).with(@current_account, "params").and_return([mock_data_point])
  end

  it { should set_the_flash(:notice) }
  it { should render_template('data_points/new')}
  it { should assign_to(:data_points, :with => [mock_data_point]) }
end

deny_access_to_visitors(:post => :create, :data => "params")

But we can do one better:

module LogInContext
  ...

  def as_user_only(params={}, &block)
    as_user(params, &block)
    deny_access_to_visitors(params)
  end
end

Now it is as simple as:

mock_models :data_point

as_user_only(:post => :create, :data => "params") do
  before do
    DataPoint.should_receive(:bulk_create).with(@current_account, "params").and_return([mock_data_point])
  end

  it { should set_the_flash(:notice) }
  it { should render_template('data_points/new')}
  it { should assign_to(:data_points, :with => [mock_data_point]) }
end

And this single test checks both that the post action is accessible to users, and also that it is not accessible to visitors. Of course these contexts can get a lot more advanced once different roles come into the picture. Here's something we're doing in our upcoming app KiNumbers:

module LogInContext
  ...

  def as_admin_or_user(params={}, &block)
    as_logged_in_user(params.dup, &block)
    as_admin(params.dup, &block)
    deny_access_to_visitors(params.dup)
  end

  def as_anyone(params={}, &block)
    as_admin(params.dup, &block)
    as_logged_in_user(params.dup, &block)
    as_visitor(params.dup, &block)
  end
end

This way there is no overhead in testing that a particular action is accessible to several different groups of users. Note that we had to call #dup on params, before passing it along, since Remarkable seems to use destructive operations on the Hash (it turned out to be empty after having been used in a describe block).

We ended up with a controller test that looks like this:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe DataPointsController do

  mock_models :data_point

  as_admin_or_user(:get => :new) do
    it { should respond_with(:success) }
  end

  as_admin_or_user(:post => :create, :data => "params") do
    before do
      DataPoint.should_receive(:bulk_create).with(@current_account, "params").and_return([mock_data_point])
    end

    it { should set_the_flash(:notice) }
    it { should render_template('data_points/new')}
    it { should assign_to(:data_points, :with => [mock_data_point]) }
  end

end

Short, easy to read, yet also very thorough. Controller tests are sexy again! Spread the word!

Jun

VTD Billing Support System


Client :Västsvensk Tidningsdistribution (VTD)

Year :2009

Web site :http://www.vtd.se/

To keep track of what to bill each month, newspaper distribution company VTD used to rely on spreadsheets emailed back-and-forth between the regional managers and the billing department. The process ment a lot of manual work for everyone involved, and was also very prone to errors. To fix both of these issues, eLabs developed a simple web application where the regional managers report their billing data each month. The billing department then has access to all the data in one location.

Using an agile approach and test-driven development allowed us to discover and verify the complex business rules. If we had tried to map them all out beforehand, some would inevitably have slipped through the cracks. The automated tests made sure that the end result - the billing data - was always correct and consistent.

Are you currently managing one of your important business processes by manually sending around Excel spreadsheets? We can help you automate the process to cut down on errors and save time. Contact us.

VTD Billing Support screenshot

May

Mittpostnummer Hyperlocal Portal


Client :ICE House AB

Year :2009

Web site :http://41104.se/, et al

On behalf of our client ICE House we developed a platform for hyperlocal portals. The portals are accessed through nearly 7000 domain names, one for each Swedish zip code. Visiting the portal for your local zip code will show you all kinds of things available near you, such as classified ads, houses on the market, current offers of local companies, etc. Think of it as the common bulletin boards at your local grocery store, on steroids.

The sites use Google Maps to give the visitors an intuitive interface to browse the site. The posts you see change as you pan and zoom around the map. The sites' content is mainly provided by agents - people local to each portal's area. We also created a simple API that the developers at ICE House can use to post things that their spiders find while they crawl around the web looking for geocoded data.

This was also the first project where we used CarrierWave, a Ruby on Rails plugin for handling file uploads developed by our very own Jonas Nicklas. We'll talk more about CarrierWave in an upcoming post on this blog.

Mittpostnummer screenshot

ICE House has created a film describing Mittpostnummer (in Swedish). Check it out below. The sites are currently in a public beta while ICE House tune their spiders and agents to fill the sites with content.

Apr

Hemmalivs Online Store


Client :Hemmalivs Skandinavien AB

Year :2009

Web site :http://www.hemmalivs.se/

Hemmalivs is an online store letting people in the Helsingborg area of Sweden do their grocery shopping from home. In addition to being very convenient for the customers, the efficient deliveries are also more friendly to the environment than if people were to drive to and from the supermarket themselves.

When the developers of their original system went out of business, Hemmalivs asked us to help them design and develop a brand new system to replace to old one.

We simplified the user interface and made the design feel more open and welcoming, while still being familiar to old customers. We used Javascript and Ajax to streamline the shopping experience for customers.

The Ruby on Rails-based backend integrates with the payment gateway Netgiro to allow customers to pay with credit card or direct payment from their Swedish bank accounts. The backend also integrates with Hemmalivs existing order and logistics system.

Hemmalivs screenshot

Mar

Mediatec Group Web Site


Client :Mediatec Group

Year :2008

Web site :http://mediatecgroup.com/

In cooperation with our parent company, Edithouse, we created a web site for Mediatec Group, one of Europe’s largest corporations working with technical solutions for event and television productions.

We developed a simple CMS that automatically gathered products, news and case studies for their different business units.

The power of Ruby on Rails allowed us to quickly develop this custom solution that was specifically tailored to their needs, instead of trying to shoehorn them into a complicated off-the-shelf solution.

Mediatec screenshot