Testing Paperclip Extensions in Isolation

thoughtbotRecently, 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.


Stephanie, Cloud City Development General Manager, believes design is both an art and a science. Cool under pressure, she’ll guide you through uncharted territory with roadmaps and deadlines while talking and listening to users and helping team members where needed. She excels at bringing design direction and order to both large-scale projects and chaotic environments. For over 10 years, Stephanie has brought quality digital products to life through great design. Her specialties include product design, Lean UX, agile, customer development, and usability. Stephanie lives in a hacker-house in San Francisco where they develop hardware, software, and experiences to improve humanity and incite creative human flourishing.


Got a question about ruby dependency management?

Or need a second pair of eyes on your Rails app?

Contact us today for a complimentary 30 minute consultation.

get in touch