Writing software is hard, and writing good software is even harder. One of our best tools for crafting quality software is the automated test. But the automated test is no silver bullet. Testing can't guarantee better software. After all, it's just a tool. It can surely help us build better software, but we first need to know how to wield it. One lesson I learned early on when testing was that focusing on the behavior is essential to successful and valuable testing. This post walks through doing just that and only that.
To help illustrate this, we're going to use an almost universal concept in software development, and one readily familiar to any Rails developer: validations.
Let's say that we have an Account that requires a name. In Rails, we'd likely have an Account model and we'd take advantage of the validation functionality that ActiveRecord provides:
In the above code, what's the behavior we're interested in? I'd say that it's that the account is not valid without a name. Or, that when we give the account a name it becomes valid:
Given the behavior we're interested in, let's look at a spec which relies on the public interface of Account to exercise and observe it:
In the above spec, we rely on three public methods on Account: the new constructor, the #valid? query method, and the #name= writer method. Let's take a quick walk through this spec.
RSpec provides a subject method which lets us specify the object that we're going to test. In this case, we're constructing a new Account that is already valid. Before our example runs, the before block will be executed and ensure that our subject is indeed valid. This gives the "requires a name" example integrity when it runs. For us, this translates to confidence that we're able to add good validation examples.
We've added an individual example for the "requires a name" behavior. The first line in the the example uses #name= to set the name of our Account subject to nil. The second line uses the #valid? inquiry method to observe that the expected behavior actually happens. Since the before block ensured that our subject was valid we know when this example passes or fails it's doing so for the right reasons.
Nowhere Are There Implementation Details
Nowhere in the spec are we relying on implementation details of Account. We're strictly sticking to the public API that Account provides. By not peering into how the validation works we've increased our ability to change the implementation over time without having to spend unnecessary time maintaining the test, that is, if as long as we don't break the behavior.
Let's walk through two refactorings that illuminate the power of this.
Let's say that DHH is really an android from a Philip K. Dick novel. He doesn't know he's an android, but his system begins to malfunction one day. Later that day he decides that the declarative validates method has to go. Fortunately, for us, he keeps the declarative validate method. So we go update our Account implementation:
We run the spec and the example still passes:
We go commit the change and prepare to move on. But first, let's take a quick moment to reflect on what just happened, because when things go well they often go unnoticed. Our spec required no maintenance. We refactored the implementation without changing the public API, and the spec required zero seconds to maintain. This is a sign that we're focusing on behavior and relying on the public interface of the objects we're testing. Or, to put another way: we've got a really good spec.
Maybe this change wasn't enough, so let's do one more to really make it stick. Continuing our earlier sci-fi storyline: android DHH has been kidnapped by cyborg DHH from a parallel universe. Cyborg DHH and has taken android DHH's spot in our world and he feels great contempt for the declarative validate method so he removes it from Rails entirely. Once again, we find ourselves in need of refactoring the implementation:
Fearing that yet another DHH will show up, we decide that our Account doesn't really need to be persisted, so we make it a plain old Ruby object, stop relying on Rails, and roll our own poor man's validation:
We go ahead and run the spec:
Again, the example passes. And again, we spend zero time maintaining the spec. Our spec is focused on the right things. We could change our implementation all day as long as we didn't break the behavior and we kept the same public API in place.
The Account name validation is super simple, both in the spec and in the implementation. But that doesn't take away from what it illustrates about having a good behavior focused spec that relies on the public API of what we're testing. If we can focus on behavior in the simple cases, it gives us a good foundation to build from when we're working with more complex logic and functionality.