giant robots smashing into other giant robots

Written by thoughtbot

qrush

Use Capybara on any HTML fragment or page

capybaras are pretty classy

I was upgrading Gemcutter to Cucumber and Capybara 1.0 yesterday from Webrat (a change long overdue!), and I discovered a neat little class within Capybara that is worth sharing. Basically, since I was moving the app from Webrat, matchers like assert_contain and assert_have_selector are no longer available. Capybara’s Node class has a great Matchers mixin with tons of goodies that can be used like so, in RSpec:

page.should have_content("This should be on the page")
page.should have_selector("a[href='http://thoughtbot.com']")

Great, but how does one use that in functional/controller tests?

Enter Capybara::Node::Simple, which I found purely by chance when source diving. This class’ docs proclaim its usefulness:

It is useful in that it does not require a session, an application or a driver, but can still use Capybara’s finders and matchers on any string that contains HTML

Bingo! Now, how to use in our test suite? We’re still on Test::Unit for Gemcutter, so I had to do the following in test/test_helper.rb:

class Test::Unit::TestCase
  def page
    Capybara::Node::Simple.new(@response.body)
  end
end

Now the Gemcutter test suite can do assertions like so:

assert page.has_content?("Rails (3.0.9)")
assert page.has_selector?("a[href='/gems/rails/versions/3.0.9']")

The whole diff is on GitHub if you’d like to see all of the changes of moving our functional tests from Webrat to Capybara.

Gabe also found out that there’s also a shortcut in Capybara for creating a Simple: Capybara.string. The docs for this show that it’s basically sugar on top of the Simple initializer:

node = Capybara.string <<-HTML
  <ul>
    <li id="home">Home</li>
    <li id="projects">Projects</li>
  </ul>
HTML

node.find('#projects').text # => 'Projects'

I think this pattern is really useful not just for upgrading suites from Webrat, but really anywhere you have an HTML fragment or string that you’d like to use Capybara’s matchers on.

dancroak

Testing Paperclip on S3 with Cucumber & Factory Girl

We’ve been using Heroku as a staging environment for our latest project. One constraint is a read-only filesystem.

The most apparent effect of this is we cannot allow users to upload files to the filesystem.

Fine. Paperclip has an S3 storage option.

Testing

Webrat has a very nice existing convention for interacting with file fields:

When /^I attach the file at "([^\"]*)" to "([^\"]*)"$/ do |path, field|
  attach_file(field, path)
end

Unfortunately, if we have an S3-backed model like this…

has_attached_file :logo,
 :path           => ":attachment/:id/:style.:extension",
 :storage        => :s3,
 :s3_credentials => {
   :access_key_id     => ENV['S3_KEY'],
   :secret_access_key => ENV['S3_SECRET']
 },
 :bucket         => ENV['S3_BUCKET']

… then we’re going to be doing a RESTful PUT to S3 during each test run.

Incidentally, those ENV variables are Heroku’s config vars. The idea is that you keep that configuration separated from your source control.

Total integration?

You could argue that these PUTs to S3 are a good thing because your Cucumber feature will represent total integration.

While that’s true, I’d rather not pay bandwidth costs and I’m comfortable as long as the correct interface to S3 was called.

So what we’ve landed on is something like this:

Given I am on the new band page
When I attach a "demo_tape" "mp3" file to a "band" on S3
And I press "Upload demo tape"
Then I should see "Band was successfully created"

The only non-standard Webrat step is our new S3 step. Let’s take a look at it:

# features/step_definitions/paperclip_steps.rb

When /^I attach an? "([^\"]*)" "([^\"]*)" file to an? "([^\"]*)" on S3$/ do |attachment, extension, model|
  stub_paperclip_s3(model, attachment, extension)
  attach_file attachment,
              "features/support/paperclip/#{model.gsub(" ", "_").underscore}/#{attachment}.#{extension}"
end

The stub_paperclip_s3 method is coming from a custom Shoulda Macro:

# test/shoulda_macros/paperclip.rb

module Paperclip
  module Shoulda
    def stub_paperclip_s3(model, attachment, extension)
      definition = model.gsub(" ", "_").classify.constantize.
                         attachment_definitions[attachment.to_sym]

      path = "http://s3.amazonaws.com/:id/#{definition[:path]}"
      path.gsub!(/:([^\/\.]+)/) do |match|
        "([^\/\.]+)"
      end

      FakeWeb.register_uri(:put, Regexp.new(path), :body => "OK")
    end

    def paperclip_fixture(model, attachment, extension)
      stub_paperclip_s3(model, attachment, extension)
      base_path = File.join(File.dirname(__FILE__), "..", "..",
                            "features", "support", "paperclip")
      File.new(File.join(base_path, model, "#{attachment}.#{extension}"))
    end
  end
end

class ActionController::Integration::Session
  include Paperclip::Shoulda
end

class Factory
  include Paperclip::Shoulda
end

Fakeweb and more conventions

We’re using the Fakeweb gem like we normally use mocking: expect that something happened, and stop it from actually happening.

We’re also leaning on conventions similar to the actor directory convention we’re also trying.

In this case, we’re expecting our features directory to look like this:

features/support/paperclip/band/demo_tape.mp3
features/support/paperclip/band/demo_tape.aac
features/support/paperclip/band/demo_tape.ogg
features/support/paperclip/band/demo_tape.wav
features/support/paperclip/user/avatar.png
features/support/paperclip/user/avatar.jpg
features/support/paperclip/user/avatar.gif

This allows us to test the expected and unexpected formats by changing this line:

When I attach a "demo_tape" "mp3" file to a "band" on S3

Factories

The reason the stub_paperclip_s3 and paperclip_fixture methods are set up as a custom shoulda macro is so that you can use them in your factory code:

Factory.define :band_with_demo_tape, :parent => :band do |band|
  band.demo_tape { band.paperclip_fixture("band", "demo_tape", "png") }
end

Go or no go?

It’s been quite useful so far for us. How are you testing Paperclip uploads to S3? Do you see any way to improve this step definition or the convention?