How Basic RSpec Works - Simplified
Recently, I had the privilege of giving a talk regarding RSpec at the Arlington Ruby meetup group. The talk was about RSpec and how you can take some next steps with it (slides here: http://rspec-next-steps.herokuapp.com.
The talk was targeted at intermediate RSpec users. There were several in attendance whom were fairly new to RSpec. This made some of the talk seem like “magic”. Based on the questions I received, I wanted to take a moment to address some of the general workings of RSpec, in order to dispel any “magic” that may seem to be happening.
It is my hope to try to show how some of this works. I won’t be covering any of the more advanced topics just yet, as the code can get a bit complicated, and the point here is to simplify how RSpec works. So bare with me and the overly simplistic implementation. The full code is available in the following GitHub gist: https://gist.github.com/4247624
First, we need to setup some methods that define the basic usage of
RSpec: describe
, subject
, before
, after
, and it
.
First up is the outer describe
:
1 2 3 4 |
|
In this example we are specifically only supporting a single describe
block with no nesting. The reason here is for simplicity. In our case
describe
is just a method that takes an object or description string
and block. Nothing special. Hopefully, this helps make it clear that we
are only dealing with a DSL for creating / setting up examples. We’ll
store away the test object in an instance variable and keep a reference
to the block for later.
Next, we setup a method for gaining access to subject:
1 2 3 4 5 6 7 |
|
If our subject is a class
, we create a new memoized instance of it.
Otherwise, we simply return the object itself.
Next is the commonly used before
blocks and the associated after
friend:
1 2 |
|
In this simplistic implementation, it is easy to see how they work. We
just keep a reference to all of the block in a normal array (in Ruby the
order of insertion is preserved) for use later. We do the same with
after
.
Last up, is the real meat of the examples, the it
block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
As with the describe
method, this takes an optional description and a
block to execute (our actual test). We’ll need access to both of these
later, and we’ll have multiple examples, so we’ll use a simple object to
keep track of each. After creating the example object, we’ll store it
in the queue.
At this point it is worth taking a moment to discuss Example#call
. We
named it call
so that accessing it is no different than a traditional
block. This makes it easier to change code later.
Inside Example#call
, we attempt to pass call
on the block that the
Example
was created with. If this raises an error, we store the
exception for access later and mark the test as failed
. Something to
note here is that the return value of the test
block is ignored. A
test is marked as passed
as long as it does not raise
any errors.
This is also how RSpec behaves.
If no block was given when we created the Example
, then we treat it as
pending
. I have omitted the pending
method, common in RSpec due to
the complexity it would add to this example.
Something else to note, since this is an overly simplistic example, we are doing everything in the global main namespace. RSpec does not behave this way, but it helps keep our example simple. Due to this, we’ll need to setup some of our variables:
1 2 3 |
|
Additionally, we don’t have any matchers defined yet. To keep it simple,
I’ll add a TestUnit style assert
matcher.
1 2 3 |
|
At this point, I hope you can start to get the picture of how our
simplified example will run. We’ll setup our run
as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
If there is no object under test defined when we run
(i.e. describe
was never called) then we raise an error. Otherwise, we output the
object under test. If this is a class
(as is usual for a top level
describe
block) then we will see the class name. Otherwise, the object
itself is output. If it is a string, we’ll get the string value,
otherwise, we’ll get the object’s #to_s
representation.
It should be noted that in the real RSpec this outputting is much more complicated and left up to various output formatters.
Next we will run the test_group
(the body of the describe
block). This
in turn call all our before
, after
, and it
methods, which set up our
environment and define the examples.
All that is left, is to iterate over the examples and run them. Here I’m
using each_with_index
solely to be able to add some debugging output
to make it a bit clearer how things are running. Normally, this would be
a simple each
iterator.
Before each test run, we make sure we have a new empty subject
. We
then iterate through each of the before
blocks in the order they were
defined. At this point we run the example. After the example runs, I’m
immediately
outputting the results to keep things simple. In the real RSpec, this is
handled by an output formatter. Finally, all of the after
hooks are
run, but in reverse defined order.
It should be noted, that here, as in the real RSpec, if any of the
before
blocks throw an exception the test fails. However, failures in
any after
block are ignored.
That’s it. We can then use this to define our sample spec:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
I hope it is clear how this spec will end up running. This is not
exactly equivalent to how RSpec will treat things (notice that we need
to explicitly clear @tmp_value
in an after
block, where RSpec will
do that for us). This is due to how RSpec creates example classses
(which we are not using) and how it binds the blocks to different
scopes; we are strictly using the global
namespace to keep the example
simple.
Check out the gist for the code and output of the sample spec: http://gist.github.com/4247624
Stay tuned for more on RSpec in the future.