In this multi-series blog post, we're going hands-on through the steps that can help transform the design of a default Rails application into one with clearer responsibilities and that is easier to test.
Have a look at the other parts, too!
You can follow along with a repository available on GitHub that contains all the code discussed here.
Active Record is the default persistency framework that comes with Rails. Using it makes it easier for us to design the data model of our applications. Reading and writing data is relatively simple, but the simplicity of Active Record's extended features shadow some of its pitfalls. These pitfalls do not surface for smaller applications, but can have an impact on our productivity as our application grows bigger.
For example, we can add custom validations whenever we want to save a record to the database. What starts as an innocent use of a feature can turn into a high-cost refactoring if we need to change the definition of what it means to be a valid record.
For that, let's have a look at the
Movie class from the example application.
Whenever a new movie gets persisted, Active Record will ensure that it has at least a name and that the release date is not before 1895. So far so good. What are the issues with the above code?
The simplicity of
validates :title is a great feature of Active Record, as it allows a simple and concise way of expressing the need of a title. For those models whose requirements change frequently, this can cause trouble, though. Especially in a test suite that creates a lot of movies. Every time we change the definition of what it means to be a valid movie, there's a chance it has a rippling effect across our code base. This is also what the code smell “shotgun surgery” identifies.
To a certain extent, gems like
factory_girl can help alleviate these issues. But before we go on and pull in more dependencies to fight symptoms, we should think about what the actual cause of the problem is.
The problem just described is not only visible in the test suite of our application. The same behaviour would be visible if a user wants to edit a program and save it without making any changes. All of a sudden a validation message could appear telling the user that the existing program she just edited is not valid anymore. While it is good that the validations work as expected, it's surprising and unexpected from a user experience perspective.
The tension between ease of use of a framework feature and maintainability stems from tightly coupled concerns about persistency and data validations. To find an alternative to that, we can ask ourselves what we usually do when things are too coupled together: we separate them.
Once we have one thing that deals with persisting a movie and another thing to validate a movie, we have done more than created better separated concerns. We also end up with two new classes that adhere better to the single responsibility principle. As a side effect, we get the benefit of better testability. Any test that's not specifically concerned about a valid movie can just create a new movie and use it.
Using the previous
Movie example, what we want to do is move all code that is about validating a movie's data into a new class.
Let's start with the first test for our new
MovieValidator has a method
valid? that returns
false, depending on whether the given movie is valid or not.
A solution we come up with to make that test pass might look like this in the end:
But what happens when a movie is not valid? We need a way to return validation messages to clients of
MovieValidator. Later, these messages could be displayed to an end-user, for example. Let's capture that with new tests that verify these messages.
To make these tests pass, we introduce the concept of a validation result.
Whenever a new requirement for the validity of a movie comes up, we can implement it isolated from all other uses of
Movie centralised in
MovieValidator. The final version of the test and the production code are available on GitHub.
Instead of rolling our own validations, there's also the option of including ActiveModel::Validations. This is a viable alternative depending on the use case at hand, as we can make use of the already existing implementation almost without change.
After extracting the validations, how does the
Movie class look after these changes?
There's some truth to the best code is no code at all. We managed to get rid of all custom logic in our model. This leaves us with
Movie only being our gateway to persistency, and nothing else.
Which tests for Active Record do we keep then?
Ideally none. But let me explain that.
No tests at all goes a bit too far. In the end we still need to make sure that a Movie gets persisted at some point. But we shouldn't verify that Active Record works as expected. That's a dependency we expect to function properly.
In an upcoming part of this series we will go into more detail of whether that's the right place for it, or if there are better alternatives.