If you wonder how Rspec is built and is curious about the internals of such a tool then this article is for you. After watching an episode from the excellent Gary Bernhard’s Destroy all software series ,i decided to blog about it to make sure i understand how it works.
We will use Minitest TestCase to test the most basic Rspec syntax. As usual let’s write two tests, one that pass and another one that fails. And we will write the code that defines the Rspec features in spec.rb. but lets start with just the tests:
12345678910111213141516171819202122
require'minitest/autorun'require_relative'spec'classTesDecribe<MiniTest::Unit::TestCasedeftest_that_it_can_passdescribe'some description'doit'has some property'doendendenddeftest_that_it_can_failassert_raises(IndexError)dodescribe'some failing test'doit'fails'doraiseIndexErrorendendendendend
Interesting, It seems like the describe method is defined somehow but the block inside is not executed. It is time to start working on that spec.rb file
1234
#lets define describe method that will pick a description and a block and let's call that blockdefdescribedescription,&blockblock.callend
let’s run our test again
12345678910111213141516171819202122
Finishedin0.001240s,1612.9032runs/s,806.4516assertions/s.1)Error:TestDescribe#test_describe_method:NoMethodError:undefinedmethod`it' for #<TestDescribe:0x007fb8158a2558> test.rb:10:in `blockintest_describe_method' test.rb:4:in `call'test.rb:4:in`describe' test.rb:9:in `test_describe_method' 2) Failure:TestDescribe#test_that_it_can_fail [test.rb:17]:[IndexError] exception expected, notClass: <NoMethodError>Message: <"undefined method `it'for#<TestDescribe:0x007fb8158a1e50>">---Backtrace---test.rb:19:in`block (2 levels) in test_that_it_can_fail'test.rb:4:in `call'test.rb:4:in `describe'test.rb:18:in`block in test_that_it_can_fail'---------------
This is the same Exception, the block inside describe gets executed but there is an undefined method it inside. The it method is pretty much the same as describe, it will take a description and a block and will execute that block. Let’s update our spec.rb:
12345678
#lets define describe method that will pick a description and a block and let's call that blockdefdescribedescription,&blockblock.callenddefitdescription,&blockblock.callend
Good this is working, i can use that describe do .... end syntax, but how about assertions, i want to be able to do something like 2.should == 2. As Usual let’s do the same and build 2 tests, a passing one and a failing one.
Finishedin0.001312s,3048.7805runs/s,762.1951assertions/s.1)Error:TestAssertion#test_that_it_can_fail:NameError:uninitializedconstantTestAssertion::AssertionErrortest.rb:17:in`test_that_it_can_fail' 2) Error:TestAssertion#test_that_it_can_pass:NoMethodError: undefined method `should' for 2:Fixnum test.rb:13:in `test_that_it_can_pass'
I see two things , an undefined method should and an undefined Exception AssertionError. Let’s update spec.rb:
1234
...#this is easy fix, just define an exceptionclassAssertionErro<Exceptionend
In order to implement the should we need to create an assertion for the object passed and it needs to work for all ruby objects so we will implement it for the Object class. You also need to remember that ‘==’ is just syntactic sugar offered by ruby and it is the equivalent of ‘equal?’. Basically, 2 == 2 is the same as 2.equal?(2) . this is very important because we will define a == method in our DelayedAssertion class. Enough talking, let’s take a look at the code in spec.rb:
1234567891011121314151617
classObjectdefshouldDelayedAssertion.new(self)endend#lets define DelayedAssertion class nowclassDelayedAssertiondefinitialize(subject)@subject=subjectend#here where the magic happensdef==(other)raiseAssertionErrorunless@subject==otherendend
Wow, we have been able to implement some major features of Rspec. In future post i will try to add support to should method such as something.should be_true or array.should have(4).things. You can also add your implementation as a comments.