One of the things that I dislike about Rails is how some actions feel untestable. Take, for example, the act of grabbing the current user.
This is bog-standard code that you’d see in a lot of Rails tutorials
and people’s codebases. What I don’t like about it is that the
current_user
method is not easily testable. Heck, which test
file do you put the tests in?
Another example is the login/logout functionality that exists in the same boilerplate way.
Fairly common eh? And yet, should the business logic of handling login and logout functionality be handled (and tested) in the controller? No, and this is a code smell.
At this point I like to create a service object that handles some common functionality found in all the above controllers, but with a nod to being easily testable and self-contained from a controller scope. Another added benefit is that the service object is typically devoid of any Rails framework calls so testing can happen quickly and without side effects because each dependency is injected into the object at runtime.
That’s a lot of code, but each method is deceptively simple. I’m
using contructor injection to specify the session, the session key
and the object that knows how to find user records. Sensible
defaults are given so that callers don’t need to specify everything
(this is one thing that Ruby does well versus, say, Java). The
session
and session_key
are used to look up the user ID
in the session.
I keep all service objects in a directory called app/services
so
that (a) they are loaded automatically by Rails and (b) they are
in a common place within the codebase. This is in contrast to putting code
in lib
, which I find to cause search nightmares. The corresponding
tests for each service object are held in test/services
.
To test the service object you will first need to tell Rake to
use the test/services
directory. Put the following code in
a file in the lib/tasks
directory and Rake will pick it
up (give the file a .rake
extension too).
With that file in place, you can now do rake
or rake test:services
and your service objects will be tested.
So how does one test a service object? It’s quite easy since the majority of the dependencies are injected. This allows you to use tools like mocha to mock out the method calls that you don’t care about, freeing you from having to tie yourself into the Rails framework. Here is what the test file looks like.
Putting this service object to use allows us to simplify the
current_user
, login and logout functionality displayed
previously. It also allows me to be more confident that each
code branch will be tested properly.
One last feature of using a service object such as SessionService
is that the
handling of the session object for holding user IDs (denoting whether
the user is logged in or out) is kept in one place. If you should ever
change session[:user_id]
to session[:admin_id]
without this
service object, you’d need to search through your codebase to find
all instances, whereas with a service object it is all held in one
place.