TDD In-Depth Rails Callback
This article started as experiments in Rails’ ActiveRecord’s Callbacks. I want to have a state-machine in part of our Rails app. The main application for this would be to know the object’s status.
There are gems out there that does exactly this and more (i.e. know which states to transition to and from)
I wanted to roll a quick thing as it’s just for an object’s
(maybe famous last words?)
I had a problem resolving a test case that would mark the
an object as
failed whenever the update failed.
The test case would be:
Seems pretty easy, right?
A simple test that is not so easy to make pass.
I found a way to the solution, and instead of just posting the solution, I will go through my thinking towards the solution.
If you want to follow along:
Installing Vagrant and Virtualbox information can be found at:
I like to have a consistent environment that is working. The environment I will be using is from a Vagrant box. The set up files are here.
With the file downloaded, run:
$ vagrant up, which will create a
working environment. Connect to the environment using:
All of the commands and code will be working from within the vagrant environment.
The source for this article is all here
If you want to work offline, say on a train traveling 60 miles / 100 miles per hour, definitely have this cloned and run vagrant up.
Add pry & rspec-rails gems
In the Gemfile, modify the
:development, :test group to be:
rspec-railsis there as it’s my preferred Ruby on Rails test framework.
pryis there as it helps me debug but also drop into a certain state to understand specific parts of the system. (Even though using a debugger is a no-no…)
After bundle has installed the pry and rspec gems, install rspec into the Rails app:
This adds hooks when creating a migration in the next step.
Create Order Model
Now that we have a new Rails app with basic testing and debugging support, let’s make a useful model.
I’m going with an order model that has these attributes:
As this is to test out and learn part of the ActiveRecord system, I want to have a minimal object. Let’s create the model first using a migration:
and edit the corresponding migration file so it looks like:
Run the corresponding migration:
Ok, now we’re ready to… write our first test!
Set up Rspec for Order
$ rails generate rspec:model <model name> command to
generate the Rspec files for the order test.
Now we’re ready to write tests!
Before writing tests, let’s define the status fields and how it will change:
|when amount > 0||
|when received == amount||
|when save fails||
I want cases where the status changes based on different fields. In this case, I want to follow the workflow of a basic order processing system.
Test: Order is
Running the tests:
Let’s go with the created condition, where an order’s
status is to
Code: Order is
So, if I were to take the easiest path to make this pass: write a
function that directly modifies the
status value before the save…
I couldn’t think of any way of doing that, not without using ActiveRecord Callbacks.
Using callbacks are a way to perform modification during certain lifecycle events of an object. From the documentation:
status, use the
before_save callback, which requires a
modify_status will be the method used for
What happens here? Before an object is saved, Rails checks if there
are any lifecycle events. In this case,
before_save is there and
modify_status method, the status is adjusted to:
Any new Order objects created will have its
status automatically set
Test: Order is
For the next test, we want the order’s
status to be set to:
pending if the amount is greater than 0.
The test for that would be:
Code: Order is
There’s two ways to make the test pass.
Now, to make this test pass, the simplist way of doing it would be
to modify the
modify_status method to:
This would be a simple way to approach this.
Another way to pass the test: use ActiveRecord’s change methods. In
this case, it would be:
This is slightly more complicated than the previous method, but I want to highlight ActiveRecord’s Dirty methods.
Which is better? In most cases, the former would be but it all depends. There are only two tests so far.
If these were the only two tests, the former would be better. As there are more tests coming, I won’t do a big refactor just yet.
Test: Order is
Now let’s add a test for the
received case, where:
Order.amount == Order.received
The test would look like:
This time, I take advantage of the
create method and set the
amount value on creation.
The received value is set afterwards, instead of with
The main reason: I want to have flexibility in how order is created. In this case, order is created directly, but from experience, I know I want to use another method or system to create orders.
Code: Order is
The simple approach would be to add onto the
and implement code as described:
Running the tests:
What’s going on??
Hmm… the test that failed is not the newest test but the first test! Why??
Well, on order create, the
amount is nil and
nil. So, they are equal!
Only after create
How can the code be written so this does not happen on create, but does happen for the pending case?
This situation is primarily caused by
nil == nil, so let’s see if we
can solve that with:
Cool, that was quick.
Test: Irrational values of amount and received
So I have experienced a case where not all values of
received, even when the values are. The main reason, the values are
stored in an irrational way, so they can never be perfectly equal.
I kind of expect this to fail… but let’s see:
Oh, I guess I was over thinking this case. So there’s no need for code to fix this test as it’s already passing.
Test: Update status to failed on save failure
One test I implement when there’s a database involved: what if the save fails?
I know databases are reliable, but who knows, what if the database runs out of space? How would errors be known?
I can write a test where the database is filled up with data, but that would put the system I am running on at risk, so I use a stub.
Code: Update status to failed on save failure
Taking the same approach this time will not work. Why?
Looking at Rails’ save documentation, it mentions:
There’s a series of callbacks associated with save. If any of the before_* callbacks return false the action is cancelled and save returns false. See ActiveRecord::Callbacks for further details.
The callback system used so far
before_save will not be active in
the event of a save, the actions will be canceled!
So, what can be done?
What happens when a actions are halted?
The whole callback chain is wrapped in a transaction. If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued.
Aaaah, the keyword is rollback.
Looking at the lifecycle events for callbacks, I notice in the list:
- after_commit/ after_rollback
So, if a
after_rollback method was there, this would be called in
save ever failed.
update_column as it would provide another mechanism to update
the status than
Let’s see if this works:
Great, tests are all passing!
Question: Is this realistic?
In a way, this is not realistic. Why? This last test kind of simulates a database failure. Where, if the first time writing the data fails, why would writing with a different method pass???
So, here is probably where good error logging service would come in handy. Something that does not use the same database to inform users something bad has happened.
Instead of writing the data, send an email!
Use Rails’ ActionMailer:
Already have too many emails? How about getting info on Slack? Here’s how to get messages in Slack
The key point: fatal errors are caught and there’s notification for them.
Note: don’t stub
Originally, I wrote the test as:
That passes as is, but for the article, I updated to:
That started failing. I remembered in Rails, to stub
save to fail,
do not stub
save, but stub:
When looking at the source for save:
Strange that stubbing
save fails, but
passes. Guess there’s additional magic under the hood…
What started out as an exercise in state transitions using
ActiveRecord callbacks ended in diving into the guts of
save method to solve a simple problem:
How to mark an item when the database fails?
The solution presented is not perfect, but I feel it is pretty good and there’s opportunity for improvements. At the same time, if the database situation is so catastrophic, nothing except working with an independent notification that is external to system like email or Slack will help.