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.
1 2 3 4 5 |
|
Here is the corresponding example that validates the functionality.
1 2 3 4 5 6 7 |
|
Adding the metaprogramming
Building upon the previous version of my_class
, let’s enhance the code and the specs to define the method when the class is initialized. The method still returns a string that matches the method name.
1 2 3 4 5 6 7 8 9 10 |
|
The specs for this class get a little more challenging, because we need to handle the case when there is no method_name
passed in and when there is one passed in.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
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.
All of the code for this example can be found in my rspec_shared_example_post repo on GitHub.