Maintaining a Large Test Suite a DRY Approach to RSpec
More often than not, tests are treated as an afterthought or a second-class citizen. They are written to fulfill a requirement or to provide a mental safety net for the additions being made. After a while, the tests can become unmanageable. It may reach the point where updating them along with the business logic is such a daunting task that you stop writing tests altogether to meet deadlines.
For any piece of code, the aim should be for quality, clarity and extensibility. After all, tests are code. Why not approach them with the same mindset you would any other part of your codebase? There are many ways to maintain integrity of your test suite as well as keep things DRY. Let’s run through a few use-case driven examples.
Using common objects across units
In testing, instances of primary and secondary objects are needed. A lot of the time you will need similar instances with some conditional state. Let’s look at a
These examples use an
Invite object created with some conditional properties and associations. We are using
factory_girl which does some of the work for us, but there are a few things that we can abstract here. Here’s another look at our example with some changes made:
In this example, we’ve added a helper method
create_invite which creates an
Invite instance with some default attributes/associations. This method also allows us to customize the object by passing in argument(s):
- traits for the factory
- attributes/associations to override the defaults
This gives us both flexiblity and consistency accross our examples. What if we wanted to use this in our
Invite spec also? Let’s see how we can do that:
We moved the
organization object and
create_invite method into what RSpec calls a shared context and included it. Also,
user was added as the first argument to
create_invite, decoupling it from the example group. Shared contexts allow commonly used variables, methods and before/after hooks to be defined and included by any context(s).
Mixins are a pattern used for abstracting out common functionality that may be reused. Consider the following mixin and spec:
This gives us coverage on the concern, but what happens when we include the it in a class like this:
Now we need to assert that a
NewsPost instance has the behavior of
Commentable. Let’s update our specs and add one for the new class:
We are still covering our base case in the
Commentable spec. Using a shared example here ensures all the classes that include the mixin are covered in regards to the default mixin behavior. Note that
it_behaves_like includes the shared examples in a nested context where as
include_examples includes them in the current context.
Testing methods similar in behavior
Recently, I noticed a small issue with one of our permission checks being more restrictive than it should have been. After diagnosing the issue, I wrote specs for the method and added the check that was missing along the way. After committing, I submitted a pull request to be approved by a teammate before merging. Another developer on the team commented on the PR and asked why I didn’t just add the spec to the pre-existing ones? Sigh. I had totally missed them because the specs were defined within a loop and the method names were being composed by string interpolation. Here is what the code looked like initially:
This was an attempt to keep the specs DRY, but a clear case for using shared examples. If left this way, I would have been adding another if level == ‘access’ check before defining the example for my change. Let’s take a look at what’s there now:
With a simple structural refactor, we were able to keep the original tests while making them more explicit. Shared examples FTW! There are some things going on here worth noting:
- Shared example groups can take arguments
- Shared example groups can be extended using a block
- it_behaves_like allows examples to be included without conflicting in the namespace
Testing driven by documentation
OrgSync has an API that allows clients to interact with our application. Documentation for the API is stored in config files. The template handler reads the data from them while displaying the docs pages. Wouldn’t it be awesome if we could just reuse that documentation in our specs? We did. Let’s check it out:
Here we have a shared example group that leverages the documented return values while testing our API serializers. These specs help to ensure that our API return values and our documentation are consistent. This is a great way to keep things DRY and maintain integrity in our application.
Testing is a must. Helper methods, shared examples, shared contexts and a mixture of them all can prove to be great tools in helping maintain a DRY test suite. The more you use them, the more creative ways you’ll come up with to leverage them. Let us know how you’re utilizing them.
Justin Powell is an entrepreneur and Computer Science graduate from The University of Texas at Austin.
comments powered by Disqus