Everything in its Right Place: Keeping Your Specs Up With the Times

Eliav Lavi
Aug 25, 2017 · 6 min read
https://cdn.pixabay.com/photo/2015/10/04/11/33/matryoshka-970943_960_720.jpg

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.

Education, Education

The scenario begins with a classic class. In our app, a is initiated with and along with , 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 method takes care of advancing the education level of a by incrementing it by 1.

Here is a possible spec for this modest class:

Since the interface for the 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 after which I begin the setup with the initialization of the test object. It still cannot be actually built since it uses as a variable — which has not yet been declared — but this is alright in the world of RSpec: it will only evaluate a variable once it is actually needed. I also use RSpec’s 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 the interface of the class. In this case, it consists merely of the instance method . I fancy single-interfaced classes (even though the accessor is also part of ‘s interface in this case; I have included it mainly in order to show the results of ), but had I had another public method in this class, I would have included another top-level block.

Since I am interested in the interface’s effect upon the instance variable, I declare it’s as a . I also declare some arbitrary for the initialization of our test using , and actually call using the clause. Then I assert that was incremented by 1 (using RSpec’s single-line-expectation syntax, which corresponds with the ). It all works as expected, and we can rest assured that our class does what we intended it to do. Only until…

Hello, Changes!

Some time has passed, during which the class received some great feedback. As often happens, new requirements then come in, complicating the process of educating a . It now feels like there’s a need for a service object to handle this process. We refactor our class to look like this:

And use this :

Our class has changed: instead of knowing how does the education process goes, it knows that it should let 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 and our 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 () does, we need to update our spec as well — the 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 blocks in order to reexamine our specs.

Naturally, the new component, , needs its own specs:

Data Gets Nested

In this spec, the matryoshka approach deepens: depends on some , which in turn depends on a (changing) . This flexibility goes hand in hand with RSpec’s construct — the interface of behaves differently if the it educates holds the value of , 1 or 3 for . Each such scenario is a ; 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.

Ruby Inside

Ruby articles and posts