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.