Everything in its Right Place: Keeping Your Specs Up With the Times
Tests are inseparable from the Ruby community: the vast majority of Rubyists — 94%, to be accurate — incorporate tests in their work. Great stats, yet they do not mean zero problems. One such problem, more prone to appear on large scale projects, is outdated specs. By ‘outdated’ I mean specs that aim to describe the doings of some class, yet the described behavior no longer derives from that class; alternatively, the described behavior was extended or changed. In this post I will try to tackle this elusive problem using some of RSpec’s basic-yet-strong features.
Code moves around. In well-maintained projects, it’s only natural. Behavior that once fitted within a certain class might belong somewhere else upon refactoring. A common scenario starts out with a single class that handles some specific flow that we know of at the moment. After a while new requirements come in; we can then make some kind of generalization of the problem and refactor that area of the code. As a result, new classes might be created and public methods might move from the existing class to a new one. That is a description of a healthy process, of course.
If you don’t practice pure TDD, though, it’s not improbable that you’ll forget looking at the existing specs after making the necessary changes in the code. Hopefully, you will add fresh specs for newly added behavior. Yet, unless the changes causes them to fail, you might not remember to rearrange the existing specs of an interface whose class has changed. After a while, your test suite will get farther and farther away from describing your app accurately.
We should aim to tailor our specs to interfaces, just as we aim to program our classes to interfaces. This approach helps in keeping specs where they belong at all times, and also makes them more readable for others. On a large scale project, this is an invaluable asset. Let’s jump in with an example.
The scenario begins with a classic
Person class. In our app, a
Person is initiated with
last_name along with
education_level, which is an integer. In the real world we would need some kind of validation on this attribute upon initialization; for reasons of brevity I will leave things simple:
That’s pretty straightforward: the
#educate method takes care of advancing the education level of a
Person by incrementing it by 1.
Here is a possible spec for this modest class:
Since the interface for the
Person class is very simple, the spec is elementary as well. The setup worth a short analysis, though. I call this spec styling “the matryoshka approach” — I try to keep my concerns as nested as possible. (If you are unfamiliar with the basics of RSpec, I recommend Relish as a learning resource).
The spec opens up with
Rspec.describe Person after which I begin the setup with the initialization of the test object. It still cannot be actually built since it uses
education_level as a variable — which has not yet been declared — but this is alright in the world of RSpec: it will only evaluate a
let variable once it is actually needed. I also use RSpec’s
described_class instead of explicitly typing the class name: if I happen to rename the class, for any reason, I will have less code spots to care about; and I have just made clear which class I am describing one line above.
Then, I proceed to
describe the interface of the class. In this case, it consists merely of the instance method
#educate. I fancy single-interfaced classes (even though the
#education_level accessor is also part of
Person‘s interface in this case; I have included it mainly in order to show the results of
#educate), but had I had another public method in this class, I would have included another top-level
Since I am interested in the interface’s effect upon the
education_level instance variable, I declare it’s
attr_reader as a
subject. I also declare some arbitrary
education_level for the initialization of our test
let, and actually call
#educate using the
before clause. Then I assert that
education_level was incremented by 1 (using RSpec’s single-line-expectation syntax, which corresponds with the
subject). It all works as expected, and we can rest assured that our class does what we intended it to do. Only until…
Some time has passed, during which the
Person class received some great feedback. As often happens, new requirements then come in, complicating the process of educating a
Person. It now feels like there’s a need for a service object to handle this process. We refactor our
Person class to look like this:
And use this
Person class has changed: instead of knowing how does the education process goes, it knows that it should let
PersonEducator take care of it and then update the relevant instance variable with the latter’s response.
The Pivotal Moment
On the testing front, our previous spec will still pass — it’s assertion is still valid. Yet, employing a unit test state of mind, we should make some adjustments. We are currently lacking a spec for
PersonEducator and our
Person spec become more of a partial-integration test rather than a unit test. There is no automatic way to be reminded to look at the spec in this scenario; when practicing interface-describing specs, we need to remember to examine the corresponding spec upon changes in the interface of a class.
This is the point when sticking to organized spec structure pays off. If we changed what our interface (
Person#educate) does, we need to update our
Person spec as well — the
describe '#educate' block, to be accurate:
This is a simple example, but even if we had a larger interface, it is easy to spot the code that calls for a changes — the spec corresponds directly with the interface, and we know we simply need to look for those
describe blocks in order to reexamine our specs.
Naturally, the new component,
PersonEducator, needs its own specs:
Data Gets Nested
In this spec, the matryoshka approach deepens:
person_educator depends on some
person, which in turn depends on a (changing)
education_level. This flexibility goes hand in hand with RSpec’s
context construct — the interface of
PersonEducator behaves differently if the
Person it educates holds the value of
nil, 1 or 3 for
education_level. Each such scenario is a
context; the elegant nested spec arrangement is much clearer and easy-on-the-eye than a flat-hierarchy array of tests, each preparing the setup from scratch.
A lot of words were written and said about writing tests by a lot of people, and it seems like these words managed to stick in the Ruby world. Maintaining specs is a different front; in this post I have tried to show an approach to making it easier and less intimidating.
If you have any remarks or questions about it, use the comments! And if you found it interesting or useful, please support it by clicking the 👏 below.