RSpec Shared Examples and Ruby Metaprogramming

Introduction

Metaprogramming is both fun and challenging all at the same time. Metaprogramming with Ruby is easier than some other languages due to its dynamic nature. However, testing metaprogamming code can be a real challenge especially covering various edge case scenarios. In these situations, I’ve come to appreciate RSpec Shared Examples to make testing metaprogramming code easier.

For the purposes of this post we’ll start with a simple Ruby class and modify it, along with the specs, to include metaprogramming and shared example.

The starting point

Let’s start with a simple class that defines one method that returns a string. This is a trivial example, but in the next section we’ll add some metaprogramming to allow the method to be created on initialization of the class.

require'spec_helper'describeMyClassDynamicdoit"creates the method with the default method name"doMyClassDynamic.new.shouldrespond_toMyClassDynamic::DEFAULT_METHOD_NAMEendit"returns the default method name as a string"doMyClassDynamic.new.send(MyClassDynamic::DEFAULT_METHOD_NAME).shouldeq(MyClassDynamic::DEFAULT_METHOD_NAME.to_s)endit"creates the method with the :dynamic_method"doMyClassDynamic.new(:dynamic_method).shouldrespond_to:dynamic_methodendit"returns :dynamic_method as a string"doMyClassDynamic.new.send(:dynamic_method).shouldeq(:dynamic_method.to_s)endend

These specs are pretty straight forward. The first and the third examples ensure that the correct method is created. The second and fourth examples ensure that the method returns the correct string value.

What I don’t like though is the duplication of code. For each scenario I want to test I need to add two more examples. This will only further compound as the functionality of the code grows.

Adding some shared example

In order to DRY up the specs and to allow for easily adding other scenarios to test, I’m going to implement RSpec Shared Examples.

You can see in this example that the specs no longer repeat in the specs. All that I have to do is to call it_behaves_like "a dynamic my class" for each scenario that I want to test. I also added a third scenario that tests the method name of :your_dynamic_method with one line of code, demonstrating how easily we can add another scenario.

By passing the method_name into the shared example, we can use the method_name parameter to ensure that the examples can test a variety of scenarios.

Normally I wouldn’t include the shared examples in the same file as the specs. I would move these to a support/my_class_shared_examples.rb file and require that file in the rspec_helper.rb file or the individual spec files themselves to keep the spec code tidy.

Conclusion

By using shared examples as described above, not only is your code DRY, but there are a couple of added benefits. First, adding additional scenarios to test requires adding one line of code to your existing specs. Second, each time that you add an example to the shared example, you can be confident that it works in all scenarios.

When I wanted to change the configuration in my CanBe gem to allow anyone using the gem to pass in the details association name, I turned to shared examples to ensure that the metaprogamming required is working properly. It greatly reduced the time to implement this functionality and ensured that it was working correctly without breaking existing functionality.