Most of us write some Rake tasks and completely forget about them. In fact, we rarely give any love to our Rake tasks. Well, I think it’s time we change that. Let’s see why and how we can test our Rake tasks.
Yes, it’s a legit question. You can always say “I already tested my classes!”. But, there are couple of reasons why you should always test your Rake tasks:
- Rake tasks is code. And all code should be tested.
- Rake tasks can use any part of your app. Models, service classes and what not. If one of the classes that the Rake task relies on changes, you have to know if it will break it.
- Rake tasks can do heavy lifting. Perhaps you have a cron on Heroku that runs an email campaign that calls a Rake task. Or you generate reports with a Rake task. This is important and you need to know if it actually works.
- Forgetting about your Rake tasks is easy. Or, if you inherit a codebase it’s easy to not even notice them when beginning the project. Rake tasks are code as well and breaking it is easy.
So, yeah, test your Rake tasks!
The Rake task
For our example application, let’s imagine we have an application where users can register. But, just like with most of the websites online, people sometimes register but they do not finish the registration process. Often, the database can be polluted with records of unfinished user registration.
The application has a Rake task that deletes users that haven’t finished their registration process.
This task is run with as a cron job. Let’s see how we can test it.
Lately, I prefer testing with Minitest. I like it because it’s really tiny, quite verbose, magic-less and it’s pure Ruby. Now, since Test::Unit is basically syntactic-sugar on top of Minitest, I will show you how to test the Rake task with it.
We will take for granted that the
User model and the
UserMailer are already
tested. Our next objective is to test this Rake task. We will need one test for
this task - it will check that the task deletes the unconfirmed user. This is
basically an integration test that will work with actual records in the database.
Whenever you are testing Rake tasks, you need to load the tasks from the Rails
application itself. Note that in your tests you should change
the name of your application.
The test will look like this:
Now, in the
setup step, we create two users - one which will be confirmed and
one unconfirmed. Then, we load the tasks from the application. This is done by
This task basically requires rake and loads each file in the
You can see it’s source here. Next, we invoke the task itself. We do this by adding:
in our test. This line locates the task by it’s name and returns a
object. Then, we call
invoke on it, which executes the task. After that we
make assertions on the data in the database, expecting the unconfirmed user to
be removed from the database.
Another approach at testing this is exctracting the logic from the Rake task to a utility class. Then, we would call the method from the class that contains the logic in the Rake task.
Let’s create a
UserRemoval class, with a class method called
Then, our Rake task would look like:
Since all of the logic in the rake task is contained in another class, we can test the class itself, instead of testing the task. This is quite trivial, since we can pretty much use the same test, with some small changes.
The test would look like this:
As you can notice, the test is basically the same, without loading/invoking the
Rake task. Instead, we just call the
Now, we could stub out the calls to the database and isolate the tests from database IO, but I usually prefer to do these cheap tests with real databse records.
Now, I know that this is quite a beginner tutorial on testing Rake tasks and I am sure it didn’t rock the world of someone adept at Ruby and testing. But, I have been asked on couple of occasions about this and have seen some questions on Stack Overflow about it so I figured it would be nice to document it somewhere.
What do you think about testing Rake tasks? Do you test them? If you do, do you write integration tests or maybe you prefer unit tests with mocks? Or, maybe you take another approach?
Let me know in the comments!