A pattern for testing class methods in ruby with rspec explicit subjects

RSpec's subject method, both implicitly and explicitly set, is useful for declaratively setting up the context of the object under test. If you provide a class for your describe block, subject will implicitly be set to a new instance of this class (with no arguments passed to the constructor). If you want something more complex done, such as setting arguments, you can use the explicit subject setter, which takes a block.

describe Person do
  context "born 19 years ago" do
    subject { Person.new(:birthdate => 19.years.ago }
    it { should be_eligible_to_vote }
    its(:age) { should == 19 }
    it "should be younger than a 20 year old" do
      twenty_year_old = Person.new(:birthday => 20.years.ago)
      subject.should be_younger_than(twenty_year_old)
    end
  end
end

The subject block is called after any befores. In fact, it is not called until it is referenced in the spec - the compact syntaxes of 'it { should }' and 'its(:property)' implicitly call subject as needed*. This means any method calls in the subject block reference the state of the system (like your db), at the time subject is called.

describe "the oldest person" do
  subject { Person.new(:birthdate => Person.oldest_person.birthdate - 1) }
  it "should be older than the next oldest person" do
    original_oldest_person = Person.new(:birthdate => 100.years.ago)
    subject.should be_older_than(original_oldest_person)
  end
end

subject thus provides a decent syntax for testing method calls directly. Sometimes, though, you have methods you want to test almost purely based on what is passed into them, with few other concerns of state, for instance, when testing class methods. These specs may have a little shared set-up needed, but really the true object under test is the method call itself. In these cases it makes sense to set that as the subject, and this leads to an effective and succinct, if slightly distasteful, pattern for setting the method arguments via standard rspec ivar trickery.

describe ".first" do
  before { Person.create_10_people }
  subject { Person.first(@number_of_people) }
  it "should return the specified number of people" do
    @number_of_people = 5
    subject.size.should == 5
  end
  
  it "should return a single person when so specified" do
    @number_of_people = 1
    subject.should be_a(Person)
  end

  it "should default to returning a single person" do
    subject.should be_a(Person)
  end
end

I was bemoaning the difference between ruby and more functional languages like JavaScript, where you do have ways of easily differentiating between a method reference and call (parentheses), and how that would make a pattern like this easier by letting you set the method as the subject and then call it in the spec. Then I realized, of course ruby has a way for creating a callable reference like this: a lambda.

describe ".first" do    
  subject { lambda { |number_of_people| Person.first(number_of_people  ) } }
  it { subject.call(5).size.should == 5 }
  it { subject.call(1).should be_a(Person) }
  it { subject.call.should be_a(Person)
end

If I continue to like this pattern enough after more usage, I'll probably see if I can get them to accept a patch to allow passing of arguments directly into subject itself. That could possibly get a little messy in the code for subject itself, but behind the scenes gymnastics to get a nice testing syntax is what rspec is all about, after all.

*The latter syntax actual resets subject, the getter, to be the result of the property access, allowing for the intriguing abuse of syntax: 'its(:property) { subject.should....'

Comments (7)

Feb 07, 2011
Tim Connor said...
Provided as a gist, alternatively: https://gist.github.com/816049
Feb 08, 2011
Theo said...
subject { lambda { |number_of_people| Person.first(number_of_people ) } }

is very verbose, you can shorten it to

subject { Person.method(:first) }

Feb 09, 2011
Tim Connor said...
Nice catch on my extra unnecessary syntax.  It is a bit of trade-off, as then you lose the explicitness of arity and argument name, but probably worth it for not having the extra lambda. 

I am working on patches for adding parameter passing to 'its' and 'subject,' which might be a cleaner alternative. Perhaps they will be accepted.

Feb 09, 2011
If you really wanted to be evil, you could make it take "example.description" as the argument, so you could do something like:

it 1 do; should be_a(Person); end

Feb 29, 2012
Craig said...
You could use the subject approach but use "let" inside a context to pass parameters to the method (the "subject"). I forked your gist, take a look at #4 which I changed: https://gist.github.com/1943109
Mar 29, 2012
_fu said...
Instead of subject.call(5), you could use subject[5].
May 09, 2012
lulalala said...
This is exactly what I am looking for: a shorter way to write utility methods tests! I am wondering if it can be made into a rspec extension gem?

Leave a comment...