We are a software consultancy based in Berlin, Germany. We deliver
high quality web apps in short timespans.

Upstream Agile GmbH

Rails: FileColumn schneller testen durch Mocks

March 11, 2007 by alex

FileColumn ist ein wirkliches zauberhaftes Plugin fuer Ruby on Rails. All unsere Bilder-Uploads haben wir bisher damit umgesetzt. FileColumn bietet Helper, um Datei-Uploads zu bauen und kann die hochgeladenen Bilder natuerlich auch wieder anzeigen. Das eigentlich schoene ist aber die Integration mit RMagick, einem Ruby-Binding fuer ImageMagick, wodurch sich Bilder on the fly skalieren sowie beschneiden lassen und die verschiedenen Versionen dann automatisch im Dateisystem landen. Alles, was man dazu braucht, ist eine Zeile Code im Model, z.B.

class User
  file_column :photo, :magick => { :versions => {
	:normal => "238x",
	:tiny => "26x35!",
	:small => {:crop => "1:1", :size => "73x73!", :name => "small"},
  }}
end

Dieser Code erzeugt neben der Originalversion 3 Kopien: Eine 238 Pixel breite und entsprechend des Bildformats hohe, eine gestauchte, genau 23 x 35 Pixel grosse und eine genau 73 x 73 Pixel grosse, beschnittene Version.

So weit alles schoen. Das einzige Problem war, dass unsere Unit-Tests durch FileColumn ganz schoen langsam wurden. Bei jedem Testdurchlauf, der irgendwas mit Bilder zu tun hatte, wurden jedes mal mehrere skalierte Versionen erzeugt, was natuerlich Rechenzeit kostete. Die Loesung haben wir letzte Woche dann endlich mal zusammengehackt, und sie sieht so aus: Ein Modul, was von FileColumn installierte Callbacks ausser Gefecht setzt, wird in ein Mock des Models eingebunden, aber der Reihe nach. Hier das Modul, gelegen in test/file_column_mock.rb:

module FileColumnMock

  def mock_file_column(attr)
    define_method "#{attr}=" do |value|
      if value.is_a?(File)
        write_attribute attr, value.path
      else
        write_attribute attr, value
      end
    end

    define_method "#{attr}_after_assign" do
    end

    define_method "#{attr}_after_save" do
    end

    define_method "#{attr}_state" do
      DummyState.new self.send(:read_attribute, attr)
    end

    define_method "#{attr}_after_destroy" do
    end
  end

  class DummyState

    def initialize(file_name)
      @file_name = file_name
    end

    def has_magick_errors?
      false
    end

    def absolute_path(*args)
      @file_name
    end
    def relative_path(subdir = nil)
      @file_name
    end

    def temp_path
      @file_name
    end

    def assign_temp(temp_path)
      nil
    end
  end
end

In unserem Beispiel wollen wir die Klasse User von ihrer FIleColumn-Last befreien. Dazu legen wir in test/mocks/test eine Datei user.rb an und fuellen sie mit folgendem Inhalt:

require 'models/user'
require File.dirname(__FILE__) + '/../../file_column_mock'

class User
  extend FileColumnMock
  mock_file_column :photo
end

Der Effekt des ganzen ist, dass die Original User-Klasse aus app/models nochmals geoeffnet und mehrere Methoden, die normalerweise automatisch beim Setzen des photo-Attributs loslaufen, umd die skalierten Versionen zu erzeugen, durch Ueberschreiben ausser Gefecht gesetzt werden.

Das war’s. Und schon fliegen die Tests wieder mit voller Geschwindigkeit.