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?
Note:
Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.