Use ActiveRecord bang methods in your tests

I’ve been working with Rails for more than 5 years now and during this journey I’ve learned the importance of building a reliable and maintainable test suite along with your development code. There are lots of resources on the web with advices on how to write tests in Rails applications. This blog post talks about one specific advice: use ActiveRecord bang methods in your tests.

If you are new to Rails, the bang methods are those whose name finish with an exclamation mark (!). For instance:

  • destroy!
  • save!
  • update!
  • validate!

And the difference between these methods and theirs counterparts is that they raise exceptions in case of failures. Using the save method as an example, here is what the documentation says:

save!:

By default, save! always runs validations. If any of them fail ActiveRecord::RecordInvalid gets raised, and the record won’t be saved.

save:

By default, save always runs validations. If any of them fail the action is cancelled and save returns false, and the record won’t be saved.

Now that you know the difference, you may ask: Why use the bang methods in tests? Let’s take a look at an example.

Imagine that we want to bill the users from our digital music service and the price depends on the number of members that signed to the plan. See below a draft of the Plan class and the tests for the Plan#price method.

All testes pass. Cool!!

Now, imagine that we add the following validation to the Member class:

After this change, our tests fail:

Failures:

1) Plan.price has more than two members should give 1 discount per additional user
 Failure/Error: expect(plan.price).to eql(19)

expected: 19
 got: 20

(compared using eql?)
 # ./spec/models/plan_spec.rb:32:in `block (4 levels) in <top (required)>'

Finished in 0.12891 seconds (files took 2.51 seconds to load)
4 examples, 1 failure, 0 pending

Interestingly, only one out of the three tests fails. If we investigate further, we will notice that due to the validation, the Members were not saved to the database and members.count is 0 instead of 3. We can fix the test by adding Members last_name attribute:

All tests pass again. We go ahead and push the changes. All good, right?

Not really! What about the two other tests? They are not failing, but they have the same issue: Members are not created and members.count is always 0. In other words, both tests are not doing what we expected them to do: they are basically testing nothing.

Now, let’s step back a little and rewrite our tests using ActiveRecord bang methods:

Running our tests again, we see a much better result. All tests failing:

Failures:

1) Plan.price has less than 2 members should return base price when one member
 Failure/Error: let(:dan) { Member.create!(first_name: 'Daniel') }

ActiveRecord::RecordInvalid:
 Validation failed: Last name can't be blank
 # ./spec/models/plan_spec.rb:8:in `block (4 levels) in <top (required)>'
 # ./spec/models/plan_spec.rb:12:in `block (4 levels) in <top (required)>'

2) Plan.price has less than 2 members should return base price when two members
 Failure/Error: let(:dan) { Member.create!(first_name: 'Daniel') }

ActiveRecord::RecordInvalid:
 Validation failed: Last name can't be blank
 # ./spec/models/plan_spec.rb:8:in `block (4 levels) in <top (required)>'
 # ./spec/models/plan_spec.rb:17:in `block (4 levels) in <top (required)>'

3) Plan.price has more than two members should give 1 discount per additional user
 Failure/Error: let(:joe) { Member.create!(first_name: 'Joe') }

ActiveRecord::RecordInvalid:
 Validation failed: Last name can't be blank
 # ./spec/models/plan_spec.rb:24:in `block (4 levels) in <top (required)>'
 # ./spec/models/plan_spec.rb:29:in `block (4 levels) in <top (required)>'

Finished in 0.12316 seconds (files took 3.26 seconds to load)
3 examples, 3 failures

Using the ActiveRecord bang methods we can see the following advantages:

  1. We are able to see the failure root cause. Instead of showing an error regarding the expected price, the tests now show the validation error message. This is extremely useful to understand the issue and fix it faster;
  2. All testes fail. Before, we left two other tests passing, which together add no value to our suite.

Unfortunately, this is very common in big test suites. In the long term, it is hard to make sure that all tests are doing their jobs by failing/passing for the right reasons whenever we make changes to the code. ActiveRecord bang methods help in this scenario by raising exceptions every time an error occurs instead of only returning false.

So, for now on remember to use ActiveRecord bang methods in your tests 🙂

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s