Ruby Redos: Rspec

I have always found Rspec syntax fascinating. There is something incredible about being able to type readable English sentences and have them do exactly what you think they will from reading them.
I wanted to take a stab at recreating something like Rspec and it was a great exercise. Test::Unit has always felt foreign and this little experiment was a great reminder of why: It ignores Ruby’s blocks.

Test::Unit works by gathering all the decendents of Test::Unit::TestCase and calling every method that starts with “test_”:

class TestMyClass < Test::Unit::TestCase
  def test_my_method1
    ... 
  end 

  def test_my_method2
    ... 
  end 
end

This approach, to my eyes, is a relative of the command pattern, which is essentially a workaround for the lack of language support for blocks. It does not make much sense to use the command pattern in Ruby but you still see it pop up all over the place.

Rspec syntax, on the other hand, is really a just a bunch of Ruby blocks:

[14] pry(main)> load 'test.rb'
=> true
[15] pry(main)> describe "Thing" do                                                                       
[15] pry(main)*   it "is nil" do                                                                          
[15] pry(main)*     expect(nil).to be_nil                                                                 
[15] pry(main)*   end  
[15] pry(main)* end  
A Thing:
is nil
-----

You will notice that describe is defined on the main object and that everything else I have put in a TestContext class to house the methods that I am expecting to be called in the blocks. Then I am using instance eval to execute the block as though it were part of the TestContext class. One other thing to point out is that calling Proc.new will grab any block that has been passed to the method:

require 'benchmark'

def describe subject
  puts "A #{subject}:"
  block = Proc.new
  Benchmark.measure do
  TestContext.new.run(block)
  end.real
end


class TestContext

  def initialize
    @examples = {}
  end

  def run block
    instance_eval &block
    @examples.each_pair do |desc, code|
      if code.call
        puts "  \033[32m#{desc}\033[0m\n"
      else
        puts "  \033[31m#{desc}\033[0m\n"
      end
      puts "-----"
    end
  end

  def it its_description
    @examples[its_description] = Proc.new
  end

  def expect thing
    Assertion.new(thing)
  end

  def be_nil
    NilMatcher.new
  end

end

class NilMatcher

  def match subject
    subject.nil?
  end

end

class Assertion

  def initialize subject
    @subject = subject
  end

  def to what
    what.match(@subject)
  end

end

I like the fact that the emphasis is on how a few methods read when chained (describe, it, expect, to, be_nil) and using those as a thin layer around blocks. This is some of the core stuff that that makes Ruby great to work with and, in my opinion, a great example of what Matz is talking about when he says “Languages can be weapons against complexity”.
He has given us blocks in the language so things as common as “wanting to execute code later” is actually part of the language. No design pattern required. The flexibility of the language also means we can write code with a very small distance between what is said and what is meant, further reducing the complexity. Both of those things are on display in Rspec.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s