
Simple Command
Anyone who has ever worked on a Rails project has surely seen so-called fat controllers, where the logic of individual actions is crammed directly into controllers that automatically have hundreds of private methods and thousands of lines of code, with actions looking like this, for example:
I have a good practice of moving all logic from controllers to external domain service classes, commands, use cases - call them what you will. When testing such controllers, I'm only concerned with the shape of the responses returned by individual actions and whether the appropriate service was called with the correct parameters. That's all.
To apply such a solution, PORO (Plain Old Ruby Objects) are sufficient, but I like to use a small, nice library called Simple Command, which extends our PORO with a few things that improve convenience and testing of such classes.
Link to the library: https://github.com/nebulab/simple_command
From the gem's documentation, we can read that:
SimpleCommand
in your class, you need to (I have a bit of trouble translating this properly into Polish) precede it with the SimpleCommand
class itselfIf you want to know what prepend
is about, check here.
.call
method defined in itsuccess?
and failure?
call
method will be available under the result
attributeerrors
attributeTesting such a class is very simple. We're really only testing 3 cases:
Of course, we can go deeper and check if some operations on other classes were called inside the command, or if the errors
array contains information about specific errors, or if the command throws exceptions appropriate for our test cases. However, these three points above are enough to feel comfortable with the implementation of the logic contained in the command.
Let's now move on to the specific case of logging a user into an API. The code I prepared is framework agnostic, which shows that SimpleCommand and the command pattern supported by this library can also be used outside of Ruby on Rails, in scripts, console applications, automations, etc.
A classic example according to the Rails way:
The login
method is quite long. There are a few ifs, some complex conditions, several renders. What if we add, for example, 2 Factor Authentication to the application? We'd have to include this logic somewhere between these ifs. Extracting this code into private methods won't fully pass the test. What if another action uses the same method, and in the meantime, we modify the method to new requirements?
Let's move all the logic to the LoginUser
command. For the purpose of this example, the User
and AuthToken
classes are stubs, their implementation doesn't concern us.
What do we have here? The command accepts parameters needed for logging in. During the execution of the logic, it throws its own exceptions, catches these exceptions, and sets error codes and responses accordingly. There are several private methods, extracted for greater code clarity.
Now let's look at the test for such a command:
The tests are very simple. First, we check the happy path, and then how the command behaves in case of incorrect login data. That's all, and that's enough.
And this is how the command call will look in the controller and the test for the login action:
And here is:
A simple scenario. At the controller level, we only check the correctness of calling the command with the appropriate parameters. And the responsibility for testing the logic and its correctness is delegated to another layer.
So, what now? When we have more such actions that use commands, we're in for some serious if-statements. We can handle this with helper methods. For the purpose of this example, I put them in an included module CommandHelpers. The command is intercepted by the handle, and it's responsible for properly handling its responses or errors. This handler can always be expanded and customized further. However, I'm presenting you with a minimal implementation version.
Then in the controller, something like this is enough:
And that's it 😀
As a result, we have encapsulation of logic in a nice class that also contains information about the success or interruption of the action, and clean and clear controllers.
Note! In the SimpleCommand repository on Github, you might notice that there haven't been any new commits for several years. The library looks unmaintained, so you might have concerns about using it. Don't worry. Just take a look at the gem's code and you'll see that the entire SimpleCommand is actually pure PORO without any dependencies. Instead of installing the gem, we could just as well include this code in some helper class in a Rails application.