Ghetto contexts in Rails tests

Written on Friday, December 26, 2008

One thing I really miss when I’m working in a Shoulda-less environment is contexts in my tests. Contexts let me properly arrange my tests in a way that makes sense, clean up my setup methods, and write/generate test names that are easily readable and immediately convey what’s meant to be tested.

Recently I submitted a Rails patch to allow for an :on option in ActiveRecord::Observer. As an example, I wanted to be able to write code like this:

    class HappenableObserver < ActiveRecord::Observer
      observe :comment, :photo, :on => :after_create
      observe :post, :on => [:after_create, :after_update]
      observe :event, :participation, :on => [:after_create, :after_update, :after_destroy]
  
      # Actual callback methods omitted....
    end

The intent being that the after_create is the only one that gets fired for Comment and Photo, Post gets after_create and after_update, etcetera, the HappenableObserver being a kind of activity feed creator. Right now, without doing it by hand or creating separate observers, this isn’t possible in ActiveRecord::Observer. When writing the patch, I ran into places where I needed an object, and didn’t actually need a different instance of it for every test method. Enter, ghetto-contexts. Here’s an excerpt from my tests for the patch:

    class ObserverTest < ActiveRecord::TestCase
  
      def setup
        @observer = ActivityObserver.instance
      end
  
      lambda do # with topic 
        topic = Topic.new
    
        test "should observe after_create on Topic" do
          @observer.expects(:after_create).with(topic)
          topic.send(:notify, :after_create)
        end

        test "should observe before_destroy on Topic" do 
          @observer.expects(:before_destroy).with(topic)
          topic.send(:notify, :before_destroy)
        end
  
        test "should observe before_save on Topic" do
          @observer.expects(:before_save).with(topic)
          topic.send(:notify, :before_save)
        end
  
        test "should ignore after_save and after_validation on topic" do
          @observer.expects(:after_save).with(topic).never
          @observer.expects(:after_validation).with(topic).never
          topic.send(:notify, :after_save)
          topic.send(:notify, :after_validation)
        end
    
      end.call

    end
    

All the greatness of blocks is often overlooked in Ruby. Since a test method that takes a block was added to ActiveSupport::TestCase, a lot of new possibilities open up. In this case, since the test is defined from a block, I can make an enclosing block, define my object as a local variable there, then define all my test methods to make use of it. I don’t need to instantiate an object that other tests don’t even use in the setup method, and I can group tests in a slightly cleaner way.

This isn’t always an applicable technique - in cases where a new Topic instance was needed every instance, this would fall over. If you need a single instance of an object for your tests, and want to organize them a little more nicely, consider it next time you’re writing a test in a testing environment that lacks actual contexts. Moving your testing to use something like Shoulda or RSpec is most likely a better long-term fix.

Comments

New Comment

    Subject:
    Your Name:
    Comment: