Custom RSpec matchers for blocks, and the Ruby magic behind their inner workings

rspec-custom-matcher-expect-block

Sections: Custom matchers and blocks How does it work?

Custom matchers and blocks

Having used RSpec as my favourite testing framework for a while now, one of the aspects that I like the most about it is that it has some very useful built in matchers that enable you to test, with some friendly and readable syntax (at least compared to other testing frameworks), expectations on what happens when a given block of code is executed, rather than simply comparing some objects. Examples of such useful matchers are change and raise_error.

If you use RSpec, you are likely familiar with these matchers and likely know that you can use them either with lambdas,

or, preferably, with a nicer syntax thanks to expect:

But what if you want to test some custom expectations when a block is executed? It’s actually pretty easy, thanks to RSpec’s support for custom matchers.

Say, for example, that you have an app ensuring that “incidents” are logged into some database table, whenever some events of particular relevance occur. Such logging could be done with a simple model named, say, Incident, so that whenever some particular condition is met or event occurs, a new instance of Incident of a given type is created in the database having as “parent” object the subject of the event that is being logged and, optionally, some other data.

It would be possible to test this kind of functionality by just checking if a new incident of the expected type and with the expected properties is actually logged when a particular event occurs, in a way similar to the following:

The example above is a very simple one just to give an idea; you might already notice though that this test isn’t complete, in that it does not actually test that the expected incident exists because of the code we are testing, and wrongly assumes that the incident we are looking for is the first/only incident logged for the test user. What if the incident is correct but -for whatever reason – already existed before the code we are testing was executed? Or what if the incident we are looking for isn’t the only one logged for the user (nor the first/last one), because other events in the object’s life also might log other incidents?

Of course, these are very simple problems that can be very easily solved with a little bit more code and one or two more expectations so to make the test complete; we could for example use the change matcher (as shown earlier) to test that a new incident of the expected type is created when we execute our block of code, and then simply test expectations on that incident’s properties. But if we are testing this logging functionality for more than a few events, we might end up with a lot of nasty duplication. After all, the code required to test this functionality would be basically the same for all the events, except that the subject of the event, and the event name, would change for each event.

A possible solution to remove duplication and hide away the logic required to test that an incident is created when expected, would be a simple helper. That would work, but an even better, nicer solution would be a custom matcher like the one shown in the code snippet below:

And, to enable the new matcher in RSpec (in spec_helper.rb):

So we now have a simple matcher for the Chronicler feature, that will execute some block (block_to_test.call) and then return true or false depending on whether the condition defined by the matcher is met or not. The condition being, of course, that an incident of a particular type with the expected data is created when the block is executed. We also have some friendly error messages that RSpec will display if the expectation defined by the matcher fails its verification. You can also notice that there’s a handy helper, chronicle_incident, that accepts as arguments the target object we want to log the incident for (assuming Incident is used with a polymorphic association to multiple models), and the name of the event. In a way, our matcher looks like an extended version of the change matcher since it too tests that an object has somehow changed due to the execution of our block, but it also tests more explicit expectations on those changes, in one go.

We can now use the matcher as follows:

The new syntax is nicer, more readable, more expressive. At the same time, we have removed a lot of potential duplication if we are going to test this event logging a lot, and we’ve hidden away the steps and expectations necessary to test that the expected incident is created. But how does this little “magic” work behind the scenes?

How does it work?

It’s actually pretty simple. If you open the rspec-expectations gem (one of the several gems that, together, make the RSpec testing framework), you’ll find the answer right away if you look a little bit in the code. Firstly, expect is nothing more than a simple macro that, given some aliases for the methods shouldshould_not (defined elsewhere) – so that we can use the syntax expect {…}.to/to_not - “attaches” these to/to_not methods to the blocks that we pass to it in our tests, by extending those blocks with these methods.

But… you might wonder, isn’t it true that only objects can be extended? And what about should/should_not? What exactly are they? As you can quickly find out from looking into the code, they are methods defined in the code as Kernel methods, so that they are available to all Ruby objects (of course besides BasicObject, which does not include the Kernel module):

Uhm…all objects? You might remember that in Ruby almost everything is an object. Well, apart from normal methods, for example… But the block we pass to expectis treated as an object, and therefore the should/should_not methods (and their aliases to/to_not) would be available to it. But why is our block treated as an object? If you look again at expect‘s signature,

you can see that the param block is defined with the unary ampersand: this is equivalent to constructs like…

and…

and…

in that, like the above three, &block also returns a proc (“converts” the block into a proc) for deferred evaluation: the proc is simply “stored” to be executed later by our matcher with call. You can find easily where this happens, by looking a little further into RSpec’s code.

Update 26/02/212: (thanks Matijs) it is actually not possible to simply pass a “normal” method as argument for expect, if that method was previously defined elsewhere with the usual def..end syntax. The reason is in how a normal method has to be passed as argument to be treated as a block. You can’t for example just pass your method as argument as you’d do for normal arguments:

The above would fail with

for the expect method, since your method would be treated here as any normal, non-block argument, while expect‘s signature only accepts a block and no normal parameters. To make sure your method is treated as a block you’d have to use the unary ampersand as follows when passing it as argument to expect:

But …this would fail too! To understand why this fails, let me remind first how the unary ampersand operator works. When you use this operator on a block, the operator converts the block to a proc. However, what happens if you use the same operator on a proc? Simple: it converts the proc back to a block.

Back to our example: when we pass our block to expect as “&block”, the block is converted into a proc because of the ampersand operator. However, expect also uses the same operator in it’s signature, so the proc it receives is basically converted back into a block before it can even use it. So the deferred evaluation has gone, and the proc, newly converted to standard block, actually gets executed.

The result is that expect will not see a proc but … the return value of the block once executed. Let’s see an example:

So in this example our method returns a number. If we try to run the test, it will fail with:

This is because due to the double use of the unary ampersand the block_to_test method gets executed and expect only sees its return value (5463), which is a number/Fixnum – not a proc.

Now, back to the main topic. Assumed it is (hopefully) clear that expect treats the given block as proc, and why, it is important to note that proc is an object too! Therefore, expect can extend it with the methods to/to_not/should/should_not, so these methods will be available for calling on the proc and will be executed in its context. When therefore we call for example the to method on our block/proc, the proc isn’t actually executed right away. Our matcher’s helper isn’t nothing more than an argument to this to method (or – depending on the case - to_not, or the original should/should_not), therefore it gets executed first.

Let’s add parenthesis as this might make things easier to follow:

With the parenthesis, it is clearer that chronicle_incident needs to be executed and evaluated first, so that whatever is its return value, this value can be passed as argument to the to method. chronicle_incident, as you can see from the custom matcher’s code, returns a new instance of our matcher, Matchers::ChronicleIncident, and this instance is what gets passed as argument to the to method.

What happens next is pretty simple too. Let’s look again at how for example should_not is defined as Kernel method (remember again that to is just an alias for should):

Let’s put aside for a moment that to is an alias and let’s imagine that this method is defined directly with the to name since it might help a little bit see what’s going on here.

For the sake of simplicity, I have removed the other params (the custom, optional, message RSpec would display, if given, and the optional block – see how the change matcher works in detail for examples of when a block might be passed as argument to it).

So when the code

is evaluated, to is executed and the instance of Matchers::ChronicleIncident passed as argument to it (and returned by chronicle_incident) is also passed to RSpec::Expectations::PositiveExpectationHandler.handle_matcher as argument, together with self.

This is a crucial bit where some might get confused (I hope it’s been easy enough to follow so far!): what is self here? self, in the method to‘s scope, is the object that receives the to “message” (in Ruby terminology) itself.

And what is that object? Surprise! It’s the block we passed to expect in first place, remember? That is, the block we want to test.

Next, let’s have a look at how RSpec::Expectations::PositiveExpectationHandler.handle_matcher is defined (again, I have removed the optional params and a few more things just to simplify):

This is the final bit that unveils the mystery. See what’s happening here? actual is, as you can easily spot, our original block that we wanted to test, so let’s rename it to block_to_test so to make, again, things easier to follow:

Remember that block_to_test is a proc, and as such it hasn’t been executed yet. But now it will, when the matches? method of our custom matcher is executed. Let’s look at it again:

Bingo! Now the original block to test will be executed thanks to call, and because we are controlling when this happens in our custom matcher, we can do whatever we want before and after it, and test whatever expectations we want.

In conclusion, I hope the explanation didn’t suck too much :) and that you found it interesting to look at how a bit of RSpec, and some Ruby magic work behind the scenes.

About RSpec’s custom matchers, I find very useful and I recommend using them a lot in that they often help reduce duplication and have a nicer syntax in our tests for improved readability, among other things.

Would like to add something to the topic or have any tips? Please share them in the comments or get in touch :)




Have your say!

Please see my comment policy if this is your first time here or if you have any questions regarding comments.