Testing Desert Plugins in Isolation

August 22, 2009 Joe Moore

At Pivotal, some of our client projects use plugins from our home-grown social networking platform and rely on Desert to tie them all together. To test this package of plugins we created a project that contains all of our Desert plugins and wrote some rake tasks that run all of their tests. Great, right?

Mostly. We want to ensure that our plugins have the absolute minimum dependencies to function. Let’s pretend we have an UserAuth plugin and a SocialPivots plugin, where UserAuth has no dependencies, but SocialPivots depends on UserAuth. We would like to test these the to plugins in isolation. But, with Desert doing it’s job so well, our UserAuth plugin could have a dependency on the SocialPivots plugins’ models or tables and we would never know it. Everything from SocialPivots is mixed-in and loaded into memory, and all of its migrations have executed, at the time we are running UserAuth’s tests.

What we need is a way to tell Desert to load only the plugin under test, plus its dependencies listed in init.rb. Hacking Desert and Rails to allow us to specify which plugins to load turned out to be pretty easy. Check it out (full gist here):

Here, we override plugin loading:

# lib/plugin_dependency_limiter.rb
class Rails::Initializer
  def load_plugins
    # Only load the plugin under test
    Rails::Plugin.new("vendor/plugins/#{ENV['PLUGIN']}").load(self)
  end

  def add_plugin_load_paths
    # Do nothing.  We'll handle plugin loading ourselves.
  end
end

We might have many plugins in vendor/plugins but we only want to test our Desert plugins. We list them in config/plugins/plugins_to_test.yml and keep track of them here:

# also in lib/plugin_dependency_limiter.rb
class Rails::Plugin
  def self.plugins_of_interest
    # Keep track of the Desert plugins we care about
    @of_interest ||= YAML.load_file(RAILS_ROOT + "/config/plugins/plugins_to_test.yml").collect
  end

  def self.tracked_plugins
    @tracked_plugins ||= []
  end

  def require_plugin_with_plugin_tracking(plugin_name)
    self.class.tracked_plugins << plugin_name if self.class.plugins_of_interest.include?(plugin_name)
    require_plugin_without_plugin_tracking(plugin_name)
  end
  alias_method_chain :require_plugin, :plugin_tracking
end

Once you can control which plugins are loaded you can expand this to dictate which routes and migrations should be run:

Routes:

# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  if ENV['PLUGIN']
    Rails::Plugin.tracked_plugins.each do |plugin|
      map.routes_from_plugin(plugin)
    end
    map.routes_from_plugin(ENV['PLUGIN'])
  end
end

Migrations:

# db/migration/001_migrate_desert_plugins.rb
class MigrateDesertPlugins < ActiveRecord::Migration
  def self.up
    Rails::Plugin.tracked_plugins.each do |plugin|
      migrate_plugin(plugin, nil)
    end
    migrate_plugin(ENV['PLUGIN'], nil)
  end

  def self.down
    raise ActiveRecord::IrreversibleMigration
  end
end

Let’s run some tests! Though we don’t provide the rake task here, it runs db:drop db:create db:migrate db:test:prepare before running the plugin tests.

First, the UserAuth plugin that has no dependencies:

$ rake testspec:plugins PLUGIN=user_auth
(in /Users/pivotal/workspace/desert_plugins)
==  MigrateDesertPlugins: migrating ===========================================
==  CreateUserAuthStuff: migrating ====================================================
-- create_table(:user_auth_stuff)
   -> 0.0023s
==  CreateUserAuthStuff: migrated (0.0026s) ===========================================

==  MigrateDesertPlugins: migrated (0.0076s) ==================================

/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I"lib:test" "/Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb"
....................................

Finished in 0.494227 seconds

36 examples, 0 failures

Now the SocialPivots Plugin, which relies on UserAuth:

$ rake testspec:plugins PLUGIN=social_pivots
(in /Users/pivotal/workspace/desert_plugins)
==  MigrateDesertPlugins: migrating ===========================================
# Look, the migrations from UserAuth!
# Look, the migrations from UserAuth!
# Look, the migrations from UserAuth!
==  CreateUserAuthStuff: migrating ====================================================
-- create_table(:user_auth_stuff)
   -> 0.0201s
==  CreateUserAuthStuff: migrated (0.0204s) ===========================================

==  CreateSocialPivotStuff: migrating ==============================================
-- create_table(:social_pivot_stuff)
   -> 0.0034s
==  CreateSocialPivotStuff: migrated (0.0036s) =====================================

==  MigrateDesertPlugins: migrated (0.0332s) ==================================

/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I"lib:test" "/Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb"
........................................................................

Finished in 1.381141 seconds

72 examples, 0 failures

I hope this code makes testing Desert plugins easier. Thanks to Pivots Adam and Edward for pairing with me through these solutions.

About the Author

Biography

Previous
Rake, Set, Match!
Rake, Set, Match!

A few days ago I finally discovered why rake db:migrate:redo consistently angers me nearly as much as watch...

Next
Mocking ScrewUnit with iSpy
Mocking ScrewUnit with iSpy

I was looking for a mocking framework to use with Screw.Unit when I found out that Rajan had ported the spy...