Recently, while refactoring legacy code in a system, we found core domain rules that were not being clearly expressed in the application, neither in the code nor the end user. As a result, the burden of knowing all of the rules was pushed onto the user, and when the system did enforce a rule, it would likely just break for the user, not help the user move forward. Unfortunately, this "legacy" code was legacy because there was no test coverage for these rules; their logic was cryptically spread throughout the code of the application in a few really large classes which mixed far too many responsibilities and abstractions.
We decided to catalog all of the rules first through exploratory testing and inquiry with our client, users, and other developers. We found several simple rules like:
- splits words on new lines
- strips leading and trailing white-space
- strips leading and trailing periods
- strips double quotes
- strips single quotes
- strips empty lines
- strips line filled only with white-space
- replaces any white-space character in the middle of a term w/a single space
- removes duplicate words
There were many more rules just like these (30 to be exact). None of them by themselves were very complex, but many of them were used in combination to make certain types of word lists. These rules were what we wanted to capture in good unit-level tests around the different types of word lists.
While writing these tests (specs) we found an interesting property of our rules. They wanted to break out of stock functionality provided by RSpec. This led us to create a new RSpec extension which proved valuable to us and may prove valuable to you. Here's a quick walk through of our experience and what led us to creating a new extension. It starts with writing specs around these rules, and the word lists that would use them.
The First Word List
The first word list was pretty straightforward. We wrote an initial spec (using RSpec) to cover the rules:
Using RSpec as-is worked well until we hit the second word list.
The Second Word List
The second word list was similar to the first, and while some rules overlapped, there were others that did not. For example, single quotes were stripped in the first word list, but they were left as-is in the second list. Initially, our first goal was to clearly express the rules in our spec so we could drive code changes. We duplicated the overlapping examples:
How Can We Re-Use These Examples?
Once we had all of the rules for both of these word lists expressed, we took a step back and asked ourselves: how can we re-use these examples? After all, we still had a third type of word list to add which would also share in overlap with many of the rules.
RSpec's Builtin: it_should_behave_like
RSpec has a built-in utility to re-use groups of examples. It works by first defining a shared set of examples:
This lets you include the shared "a word list" examples into your concrete-specs, like so:
The problem with this is that it assumes you have a collection of examples which always go together. While we often find this feature of RSpec incredibly useful, in our case it falls short of the word list rules. We don't want a collection of examples re-usable together as a set; rather, we want a collection of examples in which we can pick and choose which to run.
A New Approach Sharing RSpec Examples
To solve this problem, we created rspec-requestable-examples, which is an RSpec extension that gives you the ability to defined shared example groups where you can explicitly include which examples to run. We ended up with a new kind of shared example group:
In the above code, we created a requestable_example_for set of example groups. Inside of it we moved all of our individual rule examples from the other specs into here and changed it to requestable_it. Using requestable_it is similar to saying it except it makes the example "requestable". You can still use it if you'd like, but it won't be requestable, it will just be a normal example.
Here's how we explicitly request these new examples from our FirstWordList and SecondWordList specs:
By pulling out all of the rule examples, we kept them together in a requestable example group. This left the concrete specs to simply specify which rules to run. It could be argued that we could have pulled out only the duplicate rules and created many shared example groups (using shared_examples_for or similar).
One reason we did not do this was for clarity. We felt that it was more important to keep the rules together in a cohesive unit. After all, they are all types of rules to apply to our word lists. This also afforded us the ability to avoid having some rules defined in shared files and others rules defined in the concrete specs. With rspec-requestable-examples, we have one file with the rules, and each concrete spec simply says which ones to use. We felt this kept the specs very robust, readable, and easy to maintain.
The Third and Fourth Word Lists
Using the new concept of requestable examples gave us extreme flexibility in introducing a third type of word list:
While creating the ThirdWordList we found found that there was actually a FourthWordList as well; it was as easy to introduce another combination of rules and express it for a FourthWordList:
Experimenting, Validating, Learning
We could have written our specs and moved on, but we thought there was a better way to get more mileage out of our specs. Our initial attempt was really an experiment. Was there another way to share examples that are closely related, but which didn't always apply? Would it benefit us to try group the examples that go together -- together?
We tried to work with RSpec's built-in tools, but we wanted to take it one step further, so we explored rspec-requestable-examples. In its initial form, it wasn't nearly as clean as it is now, but we wanted to see if the idea was worth the time. It proved valuable for the word list rules and within a few weeks following we found it worked well when we had to refactor the user roles/permission system.
Since then we've put rspec-requestable-examples up on Github, cleaned up its implementation, added its own Cucumber features, tied into Travis CI, and released it as a stable and stellar RubyGem: rspec-requestable-examples.
And while this doesn't replace any functionality currently offered in RSpec, it augments what is there to provide another tool to those using it.
More info on rspec-requestable-examples
For general information check out its Github page: https://github.com/mhs/rspec-requestable-examples
For feature requests or issues please report them to its Github issues: https://github.com/mhs/rspec-requestable-examples/issues