BDD Composition over Inheritance with RSpec Shared Examples
Originally posted on Carbon Five’s Blog.
The technique of composition over inheritience is more than simply encapsulating objects into larger entities; its really about defining models as being made up of resuable behaviors. It makes sense then in Behavaior Driven Design we apply the technique not just when writing the implementations of our models but also when creating the specifications themselves. Instead of many files repeating the same functionality in large blocks of spec code, we end up with specs that look like:
This is pleasantly tighter, DRYer, and tighter code. So how did we “compose” this spec and behaviors? TL;DR
Sample Domain
Imagine we are developing a media management application. Within this system, an assignment can be created for an image into multiple galleries. This is simple enough to model.
Additionally, each of these models can be marked with metadata, which comes in three flavors:
- references to one or more creators and places.
- tags, the traditional list of strings, stored in a Postgres array and can be searched on.
- a hash-bag of simple string key-value pairs stored as a Postgres
hstore
and are also searchable.
There is one additonal important detail; not all types of metadata would be available on every type of model:
- Images can have all types of metadata; references, tags, and the hash-bag.
- Galleries can only have tags and the hash-bag.
- Assignments can only have the hash-bag.
Implementing Images
Given the above requirements, it is apparent that at some point composing with modules will come into play and we could leap to that point. But in practice, we would be following our story-based agile process, and so may end up fully developing the Image
model first.
We start by writing out a spec, using Factory-Girl to handle instance generation.
And then implement the model to pass the spec. Note the custom code below to handle the Postgres array and hstore based tags and hashbag:
Refactor into Shared Examples
With working specs and implementation in hand for all of our metadata, we can move on to adding the same but different functionality to the remaining models. And as good devs, we HAVE to start with specs that define the behavior. We could simply use the ugly technique of “copy-paste” the spec code of Image
into the specs of the Assignment
and Gallery
. But this begs the wrath of The Maintenance Gods and their acolytes, your fellow devs!
So we won’t, as we additionally follow the principle of DRY; Don’t Repeat Yourself. This is where RSpec shared examples come into play. A shared example is a collection of context and examples you can declare with a name:
The shared example can then be invoked within the context of your specs with include_examples
or (my preference) it_behaves_like
:
You can even pass in parameters into a shared example:
Using this feature we can refactor the behavior of each of the three different types of metadata into their own shared examples. Looking at the code in the original specs, we need to change two things to better support testing against any model; not calling the methods on the Image
class and using the appropriate factory when instantiating instances to test against. We do this by passing the model we are testing and the factory to use as parameters:
Now we can rewrite the spec for Image
to use these shared examples:
Even better we can now easily write the specs of the other two models:
We have effectively “composed” our specs from a set of behaviors! We also have not said anything about HOW this functionality will be achieved, maintaining the BDD mantra of testing behavior NOT implementation.
Compose with Modules (where Inheritence will fail us)
Now how to get these new specs to pass? Again the most ugly way would be to simply copy and paste but remember; those gods and devs sure are wrathful!
In all seriousness, this is where code reuse becomes important and also illustrates where composition will trump inheritence. Yes, the desired functionality can be achieved with inheritence. We could introduce a parent MetadataBase
class that all our models inherit from. Or decide that Image
descends from Gallery
which descends from Assignment
with each level adding the additional metadata implementation needed their parent does not provide. But neither correctly represents our domain; the first example exposes functionality on Assignment
and Gallery
that really shouldn’t be there while adding class bloat, and the other solution is just plain wrong!
The better solution is to encapsulate the behavior of each specific kind of metadata into seperate modules:
We can then compose our models by including the modules of behavior they actually have.
This is extremely elegant on so many levels. It adheres to the single responsibility principle with each module solely focused around the logic of the metadata it represents. It DRYs up the implementation. And the resulting code practically reads like the domain model we outlined at the top of this post:
- An image includes references, tags, and a hash bag.
- A gallery includes tags and a hash bag.
- An assignment include just a hash bag.
Getting Cleaner
Our specs could stand to be DRYed up even further. The repetitive passing in of the class we are testing and its factory now standout like a sore thumb after all our efforts. What to do?
By default the subject
of a spec of a class is an instance of that class created via a call to new
. Given that, we create a shared context that derives the model and factory from the current subject.
We can then include the shared context within each shared example and drop the need to have them passed in.
Finally, our specs drop repeatedly passing the same set of arguments to the shared examples, reading cleaner than ever:
Final Thoughts
There are further “optimizations” we could do to the code above. For example, we could also write specs for each module itself, testing all paths within it, and only compose the model specs from “coarser” variations that just verify the general functionality; this approach would speed up running the entire test suite. But these and other exercises are left for the reader to pursue.
Most important is to realize that composition over inheritence can be elegantly implemented in our codebase from the very start, resulting in well-designed and easily maintainable domain, specs and implementation.
tl;dr
- Use composition over inheritance when the latter does not correctly reflect the domain model.
- Use RSpec’s
shared_example
to define the behaviors that are shared between objects. - Compose your specs from those shared examples by invoking
it_behaves_like
. - Implement the behaviors as
Module
s that you willinclude
in your implementation.