A thirty-one line test framework
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 2end
test "one plus two isnt one" do (1 + 2).isnt 1end
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 endend
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 endend
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 endend
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:
require './freeman'
test 'A string is indeed a string' do 'This is a string'.class.is Stringend
# time ruby test.rbA string is indeed a string: trueruby test.rb 0.05s user 0.04s system 96% cpu 0.092 total
With the gem:
require 'freeman'
test 'A string is indeed a string' do 'This is a string'.class.is Stringend
# time ruby test.rbA string is indeed a string: trueruby 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!