Testing your dependencies with RSpec

I’m finding that managing my projects’ code dependencies is smelling worse and worse as time goes on. Code bases get bigger and acquire libraries as they grow; a part of your project sits untouched for a few months and its particulars leave your medium-term memory, and so on.

In Rails, we can freeze lots of stuff to our vendor directories. I do that as much as possible—gems that I only use for Rails apps get frozen to vendor/gems and then uninstalled system-wide; I use the gemsonrails plugin for this. If the little gem bits aren’t necessary, you can just pistonize a repository. Old news.

That’s not going to fly for platform-compiled gems, or even compiled libraries that aren’t gems at all (since you’re possibly running several different platforms between development and production). So I’ve been cooking up ways to keep myself sane:

The Simplest Thing That Could Possibly Work, I think, is just a quick test failure when a dependency is missing. If you’re already autotesting locally, and automatically running your test suite on each production machine as part of your deployment recipe, a quick, obvious exception could save you a little misery.

What I mean by “obvious”

This all came about because I went through two development platform switches recently: first, a clean install of Leopard, and just last week, a move to Intel from my old PowerBook. Both of those hosed my gems, and although I got test failures for each “broken” part of the app, certain libraries’ lazy/quiet-loading techniques don’t raise exceptions in a way that’s obvious.

For example, Rick Olson’s fantastic attachment_fu plugin is meant to work just fine for non-image files, so if you don’t have a compatible image processing library installed, it’ll just skip the thumbnailing for images and move right along. So my image-uploading tests failed on not creating the right number of files and records. It took me way too long to figure out what was going on, so I think it’d be better if I was checking for known dependencies directly.

First try

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe User do

  it "depends on one of three image processing libraries" do
    processors = %w(image_science RMagick mini_magick)
    lambda {
      begin
        require processors.shift
      rescue LoadError, MissingSourceFile => e
        retry if processors.any? or raise e, "Make sure an image processing library is available"
      end
    }.should_not raise_error
  end

end

Pretty good, although so much space between it and end makes me sad. Also, attachment_fu’s requirements are kind of an edge-case; I want to be able to spec a requirement for only one library, or several all at once.

Less sadness with matchers

Read up: if you aren’t using matchers, you aren’t using RSpec.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
describe User do
  it "depends on an image processing library for attachment_fu" do
    one_of(:image_science, :RMagick, :mini_magick).should be_loadable
  end

  it "depends on SHA libraries for password hashing" do
    both_of('digest/sha1', 'digest/sha2').should be_loadable
  end
end

describe Event do
  it "depends on chronic for date/time string processing" do
    :chronic.should be_loadable
  end
end

describe Post do
  it "depends on a text processing library for Markdown support" do
    either_of(:maruku, :RedCloth).should be_loadable
  end

  it "depends on some XML libraries" do
    all_of(:hpricot, :builder, :haml).should be_loadable
  end
end

The matcher I wrote to do this is a little beefy, around 60 lines. To check it out, you can grab it from svn (or in the <3 warehouse), or from pastie.

I’m now using this all over the place, and it’s saved me at least a couple headaches. It’s really helpful for making sure your CI and deployment environments are up to spec, as well.

I’m sure there’s more to do—like checking gem versions. How are you checking your dependencies from platform to platform?

Comments

  1. Jean-Michel GarnierFebruary 04, 2008 @ 09:03 AM

    I am using http://geminstaller.rubyforge.org/ which was built with RSpec has a cruisecontrol running to check that it works with all versions of rubygems and has the following features:

    *  Automatically install the correct versions of all required gems wherever your app runs.
    * Automatically ensure installed gems and versions are consistent across multiple applications, machines, platforms, and environments
    * Automatically add correct versions of gems to the ruby load path when your app runs (‘require_gem’/’gem’)
    * Automatically reinstall missing dependency gems (built in to RubyGems > 1.0)
    * Automatically detect correct platform to install for multi-platform gems (built in to RubyGems > 1.0)
    * Print YAML for “rogue gems” which are not specified in the current config, to easily bootstrap your config file, or find gems that were manually installed without GemInstaller.
    * Allow for common configs to be reused across projects or environments by supporting multiple config files, including common config file snippets, and defaults with overrides.
    * Allow for dynamic selection of gems, versions, and platforms to be used based on environment vars or any other logic.
    * Avoid the “works on demo, breaks on production” syndrome
    
  2. bryanlFebruary 09, 2008 @ 10:22 AM

    This appears to work, but it feels nasty because you are mixing dependencies with your example behaviors.

  3. Chris KampmeierFebruary 09, 2008 @ 02:49 PM

    @Jean-Michel: that looks really interesting. I’ll check it out.

    @bryanl: can you elaborate? I feel that since it’s a behavior of my app to try and load (for example) a text-processing library, it’s reasonable to write a spec. I don’t think it looks “nasty” at all.

    If there’s a code smell, it’s that it should be the responsibility of the app’s environment to make sure dependencies are loadable (through initializers, code in vendor, etc.), and the responsibility of the models to actually do the require calls. So the specs I wrote above should perhaps describe the initialization process, with another set of specs ensuring that each model actually has code to require its dependencies. That seems a little over the top, though.

Post a comment

Comment
(not published)
(optional)