Recently, Cloud City Development was tasked with a project that included cropping an image upload in a number of squares of varying sizes based upon user selection. In order to accomplish this, we set out to write an extension to the paperclip library, which can be a hassle. Because this project already used paperclip, switching to something like dragonfly or carrierwave was not an option. This left us with test-driving the implementation with paperclip in RSpec.
Approach for Testing a Paperclip Library Extension
Here's an example of how I would test an extension to the paperclip library.
I've taken somewhat of a hybrid approach to testing this library extension here—I'm requiring paperclip and stubbing the implementation details of the rails model. The thing that I'm most concerned with here is not how this integrates with the application I've built, but how I'm generating the arguments for ImageMagick. Secondarily, the contract with direct collaborators is valuable, but outside of the scope of this post.
To generate a model-like object to test in isolation, I decided to just use a Struct object, so that I can test different values without stubbing repeatedly. I certainly would love to hear a better way to do this if anyone can provide suggestions. I wanted an object that I could initialize like a factory object without all of the weight of active record and unnecessary implementation details of the real model. Additionally, I wanted to be able to pass in the faux model object directly to the paperclip cropper.
Implementation for Testing a Paperclip Library Extension
Without further ado, here's the implementation:
module Paperclip
class Cropper < Thumbnail
def initialize(file, options = {}, attachment = nil)
super
if target.cropping?
@current_geometry.width = (target.img_w.to_f * target.ratio).to_i
@current_geometry.height = (target.img_h.to_f * target.ratio).to_i
end
end
def transformation_command
if crop_command
scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
trans = []
trans << "-coalesce" if animated?
trans << "-auto-orient" if auto_orient
trans << crop_command
trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
trans << '-layers "optimize"' if animated?
trans.flatten!
trans
else
super
end
end
def crop_command
if target.cropping?
[" -crop", "#{(target.crop_size.to_f * target.ratio).to_i}x#{(target.crop_size.to_f * target.ratio).to_i}+#{(target.crop_x.to_f * target.ratio).to_i}+#{(target.crop_y.to_f * target.ratio).to_i} +repage"]
end
end
def target
@attachment.instance
end
end
end
Here's the spec file:
require 'paperclip'
require_relative '../../../lib/paperclip_processors/cropper'
describe Paperclip::Thumbnail do
describe Paperclip::Cropper do
before do
@file = File.new( File.join( File.dirname(__FILE__),
"../../../spec/fixture_images/image.jpeg"),
'rb')
Attachment = Struct.new(:img_w, :img_h, :crop_x, :crop_y, :crop_size) do
# This struct is a barebones implementation of the
# correlated model that has an attachment to be cropped
# The img_w is the image width of the scaled image used
# The img_h is the image height of the scaled image used
# crop_x is the x offset that is used to select the crop area
# crop_y is the y offset that is used to select the crop area
# crop_size is the length of a side of the crop area, since
# our crop is always square, only one dimesnions is given here
def instance
self
end
def ratio
# Ratio is a method on the model that correlates the ratio of
# the original image to the image used to generate the crop
# dimensions. The original image under test has a width of 1280
# Here's the original ratio method:
# image_geometry(:original).width / img_w
# Instead, we stub this method for testing.
(1280 / img_w)
end
end
end
let(:target) {Attachment.new(625, 425, 125, 125, 50)}
subject { Paperclip::Cropper.new @file, {:geometry => ''}, target}
describe "#crop_command" do
context "with a cropping" do
before do
target.stub(:cropping?).and_return(true)
end
it "returns a array of commands" do
subject.crop_command.should eq [" -crop", "100x100+250+250 +repage"]
end
end
context "without a cropping" do
before do
target.stub(:cropping?).and_return(false)
end
it "returns nil" do
subject.crop_command.should be_nil
end
end
end
describe "#tansformation_command" do
context "with a crop" do
before do
target.stub(:cropping?).and_return(true)
end
it "returns an array of options" do
subject.transformation_command.should eq ["-auto-orient",
" -crop",
"100x100+250+250 +repage"]
end
end
context "without a crop" do
before do
target.stub(:cropping?).and_return(false)
end
it "returns the value of super" do
subject.transformation_command.should eq ["-auto-orient"]
end
end
end
end
end
Testing paperclip in isolation has made it easy and even fun to extend an otherwise tedious and error-prone situation of extending a library that was not originally engineered to be extended in this way.