In Part 1 of this series I began looking at how RSpec might be implemented, such that a block of code such as the below would print out a pass/fail statement to the console:
RSpec.describe 'The function add' do context 'When a given two numbers' do it 'sums the numbers' do expect(add(40, 2)).to eq(42) end end end
I have been implementing my own version of RSpec, called FooSpec, which currently prints the descriptions for the tests, but does little else:
class FooSpec def self.describe(message, &block) puts message FooSpec.class_eval &block end def self.context(message, &block) puts message FooSpec.class_eval &block end def self.it(message, &block) puts message puts 'PASS!' end end
Now it's time to look at how the
it method should actually work. A block passed to the
it method needs to be able to access the method
expect and also
eq so I'll have to make sure this block is executed within the right context, something I talked about in my previous blog post.
But I'll also have to take a look at the return value of
Here are a few things I learned when I was implementing these steps.
In Ruby, anything that begins with a capital letter, including the names of classes and modules, is a constant. This means that what it refers to shouldn't be changed. You can change its reference but you get a warning.
Foo = 'bar' Foo = 'cat' # (irb):2: warning: already initialized constant Foo # (irb):1: warning: previous definition of Foo was here
You can nest classes and constants within other classes, and when you do that, you cannot simply access these nested classes/constants from "the outside":
class Foo A = 'cat' class Bar B = 'foo' initialize(val) @val = val end end end puts A # uninitialized constant A (NameError) Bar.new # uninitialized constant Bar (NameError)
To access nested constants, use their Path, which is denoted with two colons:
puts Foo::Bar::B # 'foo' puts Foo::A # 'cat'
Often Modules are used as namespaces, to keep a collection of constants together under one roof, and stop them polluting the global namespace. (Modules are designed for grouping functionality, whilst classes are designed to be instantiated). For example, if you wanted to create a constant called
Number as part of your library, this could potentially conflict with a constant defined somewhere else as it's a pretty common word. So you'd probably namespace it inside an appropriately named module:
module MyNumberModule class Number ## methods etc. end end
raise an Exception. However, Ruby does have a throw/catch mechanism too, and this should be used when you want to exit out of deeply nested logic early, for example out of a loop inside a loop. You can
throw inside the loop, halting execution, and
catch outside to continue control flow.
catch :my_num do 10.times do |i| puts i 15.times do |j| puts j if j > 4 throw :my_num end end end end # more code...
You can define your own named exceptions, which is better than raising a generic Exception, because when you rescue from it, you know you're only rescuing from the error you raised, and not something else which might have gone wrong elsewhere, which you would genuinely want to be alerted to.
To create an Exception namespaced within a class/module:
class FooSpec class FailedTestCase < Exception end end
In this case my
FailedTestCase inherits from the
Exception class so it will have the same behaviour and methods, just a different class name.
In the end, I extended the
it method to evaluate the block passed to it in the context of a class called
class FooSpec def self.describe(description, &block) puts description FooSpec.class_eval &block end def self.context(description, &block) puts description FooSpec.class_eval &block end def self.it(description, &block) puts description begin TestCase.class_eval &block puts 'PASS' rescue FailedTestCase => e puts 'FAIL' end end class TestCase # more methods... end end
I actually decided to namespace
TestCase inside the
FooSpec class, but above I didn't need to reference it with
FooSpec::TestCase because when I referenced it I was inside the
FooSpec class so Ruby already knew to look inside that class for the
So what does the
TestCase class do? Well, we're gonna be executing the code
expect(add(40, 2)).to eq(42) inside this class, so we would want to see the
expect method and the
eq method defined in here.
My thinking was that
expect should take a value, and return an instance of a new type of class. This class has to have a
to method, and the
to method will be passed the return value of the
At some point, I'm expecting an exception to be raised since I'm attempting to rescue from it within the
it method. Depending on whether an exception is raised or not, I determine whether the test passed or failed.
The implementation is still quite basic at this stage, but it's probably a bit too much to post inline here. If you want to take a look, you can see the full code here.
Come back for part 3 where I'll be adding more equality matchers!