Building RSpec with Fix

First of all, to avoid confusion in the Ruby community, please note that:

The code and the gem that I’m going to write about is not coming from rspec.info’s rspec, but from a personal project named r_spec (for Ruby Spec).
My r_spec project is totally independent from rspec.info, even if its interface is directly inspired by rspec.info’s rspec.
Also, while the namespace of both gems is the same (I follow the gem naming convention), the r_spec internal is however quite different than rspec.info’s, as shown in this article.

Having said that, I am pleased to blog about how to build an RSpec clone that can handle some basic features, and maybe replace the original in your next project, who knows?

But for now, let’s hack some code!

Initial setup

Instead of re-inventing the wheel, we will include those small, cryptographically signed and well focused gems coming from a few Ruby Fix projects:

  • fix, a fresh new specing framework.
  • fix-expect, to provide the expect syntax.
  • fix-its, to provide “its” method for attribute matching.
  • matchi-rspec, to extend matchers with some RSpec’s ones.

To start with, our Gemfile could be:

source 'https://rubygems.org'
gem 'matchi-rspec', '~> 0.0.2'
gem 'fix-expect', '~> 0.3.0'
gem 'fix-its', '~> 0.4.0'

And instead of manually requiring all these dependencies, we’ll ask Bundler to do it for us:

require 'bundler/setup'
Bundler.require

At this point, we can define our namespace. Let’s call it RSpec.

module RSpec
end

It will have to include a “describe” class method, taking a described class and a block of specs to execute. The Fix’s Test class should do the job.

Also, after reporting the results, the test must exit with a success or a failure termination status.

Here is an implementation:

def self.describe(front_object, options = {}, &specs)
t = ::Fix::Test.new(front_object, options, &specs)
print "#{t.report}" if options.fetch(:verbose, true)
exit t.pass?
end

Now, let’s pretend that our application is the following:

app = 'Hello, world!'

A test set could thus be:

RSpec.describe String do
context 'I am talking to Alice' do
let(:person) { 'Alice' }
it 'replaces "world" by "Alice"' do
value = app.gsub('world', person)
expect(value).to eq 'Hello, Alice!'
end
end
  context 'I am talking to Bob' do
let(:person) { 'Bob' }
it 'replaces "world" by "Bob"' do
value = app.gsub('world', person)
expect(value).to eq 'Hello, Bob!'
end
end
end

Is it passing?

$ ruby test.rb
..

Ran 2 tests in 0.02856 seconds
100% compliant - 0 infos, 0 failures, 0 errors

No surprise, it is. One thing that can be noted, however, is the presence of those two contexts:

  • I am talking to Alice
  • I am talking to Bob

They were able to ensure an isolation of the tested code. This protection was useless due to the absence of side effects, but let’s try once again with the “gsub!” method. 💩

What did you RSpec?
A comparaison between the behavior of a fix-based script (named r_spec) and rspec.

As we can see, the build is passing against our testing script, but no longer against rspec.

Indeed, despite a test set with contexts, rspec don’t evaluate the code in isolation to prevent side effects.

Thus, the behavior of our clone does not behave in quite the same way, and that’s fine.

Now, we could continue with a “before” method for test setup, a “describe” method to group a set of related examples and a “subject” method to override the implicit receiver… by overriding the Fix::On class.

In the same fashion, the “described_class” implementation could be done by overriding the Fix::It class.

Showtime!

In addition to “its” and “let” keywords, it is all the Fix’s syntax which becomes available, so…

What about replacing the flat-expect syntax for the benefit of RFC 2119’s keywords to start qualify your expectation?

Conclusion

This experimental script living on GitHub demonstrates that we can build great tools with some super simple pieces of code that everyone can read and understand.

Such tools should be accessible for everybody, including less experienced developers. By the way, did you read some rspec code? Anyway,

I don’t always test my code with RSpec, but when I do I do it with r_spec.

For any questions or concerns, please do not hesitate to get in touch.

Happy testing!