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.