public
Last active

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

  • Download Gist
1_intro_to_subject.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# 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
2_delayed_execution_of_subject.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# 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
 
# *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....'
3_subjecting_a_class_method_itself.rb
Ruby
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
# 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
4_using_a_lambda_for_brevity_and_directness.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12
# 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
5_in_conclusion.txt
1 2 3 4 5
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.

I found your post when I was trying to avoid instance variables to pass 'arguments' to the subject. I like your approach and I'm using it right now. That's clever. Thnx.

Thanks, I had forgotten about this. I would rework #3 to use let(), of course.

#4 is exactly what I was looking for. Still no support for it out-of-the-box...

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.