A thirty-one line test framework

I’ve been reading “Metaprogramming Ruby” the last couple days, and realizing that a lot of what makes Rspec amazing is its usage of metaprogramming. I thought it would be interesting to try and write my own test framework.

Surprisingly, it only took a couple minutes to write something quite powerful. freeman is the gem that I produced.

It’s very simple - it extends the Object class to add methods for #is and isnt, and the Kernel class for a test method.

test "one plus one is two" do
(1 + 1).is 2
end
test "one plus two isnt one" do
(1 + 2).isnt 1
end

Each test creates a Struct, which takes the name of the test and the do..end block (the code itself), and evaluates it as true or false. This is what is a test is, after all!

Here’s the definition of should in Rspec:

def should(matcher=nil, message=nil)
RSpec::Expectations::PositiveExpectationHandler.handle_matcher(subject, matcher, message)
end

And the definition of RSpec::Expectations::PositiveExpectationHandler:

class PositiveExpectationHandler < ExpectationHandler
def self.handle_matcher(actual, matcher, message=nil, &block)
check_message(message)
::RSpec::Matchers.last_should = :should
::RSpec::Matchers.last_matcher = matcher
return ::RSpec::Matchers::BuiltIn::PositiveOperatorMatcher.new(actual) if matcher.nil?
**match = matcher.matches?(actual, &block)**
return match if match
message = message.call if message.respond_to?(:call)
message ||= matcher.respond_to?(:failure_message_for_should) ?
matcher.failure_message_for_should :
matcher.failure_message
if matcher.respond_to?(:diffable?) && matcher.diffable?
::RSpec::Expectations.fail_with message, matcher.expected, matcher.actual
else
::RSpec::Expectations.fail_with message
end
end
end

The simplest version? It compares the “actual” with the provided block, checking for equality. Here’s the freeman version:

module Kernel
def test(description, &block)
KFTest.new(description, block).run
end
end
KFTest = Struct.new(:description, :block) do
def run
status = block.call ? true : false
puts '#{ description }: #{ status }'
if status.is false
line = block.source_location.join(': ')
puts ' from #{ line }'
end
return status
end
end

Some of the things are extra: the puts statement is just to have some sense of output - I considered taking it out and just returning true or false but that’s not super helpful when you’re writing a separate test file.

I love Rspec. I wrote freeman because I want to understand how Rspec works in a more well-rounded route. In the meantime, I’ve begun integrating freeman in some personal scripts that don’t require full testing frameworks, and I’m impressed with how fast it is.

A simple test with freeman.rb required outside of the gem:

test.rb
require './freeman'
test 'A string is indeed a string' do
'This is a string'.class.is String
end
# time ruby test.rb
A string is indeed a string: true
ruby test.rb 0.05s user 0.04s system 96% cpu 0.092 total

With the gem:

test.rb
require 'freeman'
test 'A string is indeed a string' do
'This is a string'.class.is String
end
# time ruby test.rb
A string is indeed a string: true
ruby test.rb 0.23s user 0.05s system 98% cpu 0.284 total

It’s surprising the amount of time using Bundler can add to the script. Either way, it’s quite quick!