Swombat

wombat + s 
« Back to blog

Getting rspec before(:all) and nested contexts working together

As posted to the rspec-users mailing list:
 
> Like everyone (?), I use nested contexts to keep my specs well organised and tidy.
>
> However, I have a problem. I have various sets of specs that needs to perform very time-expensive operations to set up the fixtures that will be examined in the tests. Two specific examples: testing access control logic (requires creating a whole tree of items to verify the correct access level against each item), and project archival (which creates a project, fills it with test data, archives/zips the project contents, then unzips them for examination).
>
> I tried using before(:all) to set up those costly fixtures, however I hit upon a feature of rspec that made that less than successful:
>
> When using before(:all) along with nested contexts, rspec actually re-runs the before(:all) before each sub-context. So if, like me, you have your specs neatly organised in sub-contexts, the before(:all) is actually re-run many times.
>
> Interestingly, when a before(:all) is run in the root context, rspec does not actually remove the data from the database when re-running the before(:all). "Great," I then thought, "I can just detect whether the data is created and decide whether or not to create the objects on that basis". Not so fast, though: Rspec may not clobber the database, but it does clobber instance variables. In the case of the access control test, there's about 40 different instance variables, so keeping track of them all manually in some global variable outside of rspec would be messy to say the least...
 
Turns out, I couldn't be bothered to wait for a reply, so I wrote a workaround. It's not very pretty, and a bit hackish, but here it goes.
What I've done is written a "Hoover" class that will hoover up instance variables from the root context and save them for later use.
 

 
class InstanceVariableHoover 
 
 @@variables = false 
 
 def self.empty? 
  !@@variables 
 end 
 
 def self.save_instance_variables(object, &block) 
  puts @@variables.inspect 
  if @@variables 
   @@variables.each do |var, val| 
    object.instance_variable_set(var, val) 
   end 
  else 
   yield 
   @@variables = {} 
   object.instance_variables.each do |var| 
    @@variables[var] = object.instance_variable_get(var) unless 
var.starts_with?("@_") || var == "@method_name" 
   end 
  end 
 end 
 
end 

 
This is all great, unless, like most normal people, you like your specs to run more than once, because the database doesn't get clobbered between requests, so you'll fail various uniqueness constraints if, for example, you're creating users and the like.
 
The solution is, unfortunately, to manually clobber the objects that you know might be lying around, and to make sure the objects you create will not interfere with your other specs. Moreover, there's another subtle problem - each set of specs that needs a different before(:all) block will need its own hoover. Here's a full length example of this at work:
 
 
require File.dirname(__FILE__) + '/../spec_helper' 
 
class BarSpecHoover 
 @@variables = false 
 
 def self.empty? 
  !@@variables 
 end 
 
 def self.save_instance_variables(object, &block) 
  if @@variables 
   @@variables.each do |var, val| 
    object.instance_variable_set(var, val) 
   end 
  else 
   yield 
   @@variables = {} 
   object.instance_variables.each do |var| 
    @@variables[var] = object.instance_variable_get(var) unless 
var.starts_with?("@_") || var == "@method_name" 
   end 
  end 
 end 
end 
 
class FooSpecHoover 
 @@variables = false 
 
 def self.empty? 
  !@@variables 
 end 
 
 def self.save_instance_variables(object, &block) 
  if @@variables 
   @@variables.each do |var, val| 
    object.instance_variable_set(var, val) 
   end 
  else 
   yield 
   @@variables = {} 
   object.instance_variables.each do |var| 
    @@variables[var] = object.instance_variable_get(var) unless 
var.starts_with?("@_") || var == "@method_name" 
   end 
  end 
 end 
end 
 
describe "Bar" do 
 include GenericSpecHelper 
 
 before(:all) do 
  if BarSpecHoover.empty? 
   User.delete_all 
   Project.delete_all 
  end 
  BarSpecHoover.save_instance_variables(self) do 
   @user = confirmed_user_with_details("bar-test-01") 
   puts "Running Before-All 1" 
  end 
 end 
 
 describe "seriously" do 
  it "should behave correctly" do 
   @user.should_not be_nil 
   @project.should be_nil 
  end 
 end 
 
 it "should behave correctly" do 
  @user.should_not be_nil 
  @project.should be_nil 
 end 
end 
 
describe "Foo" do 
 include GenericSpecHelper 
 
 before(:all) do 
  if FooSpecHoover.empty? 
   User.delete_all 
   Project.delete_all 
  end 
  FooSpecHoover.save_instance_variables(self) do 
   @user = confirmed_user_with_details("foo-test-01") 
   @project = create_project 
   @user = nil 
   puts "Running Before-All 2" 
  end 
 end 
 
 describe "seriously" do 
  it "should behave correctly" do 
   @user.should be_nil 
   @project.should_not be_nil 
  end 
 end 
 
 it "should behave correctly" do 
  @user.should be_nil 
  @project.should_not be_nil 
 end 
end 

 
I'll grant you that it's not the prettiest thing in the world, but at least it works, and can save oodles of time on your specs. I hope it helps someone!

Comments (0)

Leave a comment...

 
Got an account with one of these? Login here, or just enter your comment below.
Posterous-login    twitter