This post is out of date

This post has been migrated from my old blog. It may have broken links, or missing content.

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!