Rspec expect to change not returning the correct result

Viewed 270

I can't figure out why the rspec expect to change matcher is not working, even though I replicated the logic and I am certain the code is correct.

Here's what I have:

class SubscriptionItem < ApplicationRecord
  after_commit :create_customer_quotas, on: :create

  def create_customer_quotas
    CustomerQuotaService.create_from_subscription_item! self
  end
  def self.create_from_subscription_item!(subscription_item)
    customer = subscription_item.customer
    subscription_item.quotas.each do |quota|
      CustomerQuota.find_or_create_by! quota:, subscription_item:, customer:
    end
  end

In my test, I have this:

expect { subscription.save! }.to change(CustomerQuota, :count).by(subscription.quotas.count)

and I get this failure:

expected `CustomerQuota.count` to have changed by 1, but was changed by 0

However, when I replicate the expect to change logic in the test like so:

        count_before = CustomerQuota.count
        subscription.save!
        count_after = CustomerQuota.count

        puts "before=#{count_before} && after=#{count_after}"

the output is: before=0 && after=1

So it's clearly working. Why is the expect to change not working as expected?

2 Answers

I suspect it is due to after_commit callback, as it dosen't triggered in rspec if we use transactional fixtures.

We need to trigger after_commit callback manually via ar_object.run_callbacks(:after_commit)

expect { subscription.save!; subscription.run_callbacks(:after_commit) }.to change(CustomerQuota, :count).by(subscription.quotas.count)

OR you can use the test_after_commit gem, which would hook up the after_commit callback as soon as transaction is committed. Bundling this gem would do run after_commit callback automatically without any manual trigger. This will be no longer needed on rails 5.0+

If you don't want (or you can't) to change code, just go with Sampat Badhe answer (use test_after_commit or upgrade to rails >= 5.0.0) and stop reading this: This answer is about software architecture and not about solving your rspec problem.

But what I wonder is.... is it really needed to be done in an after_commit callback? What if CustomerQuotaService.create_from_subscription_item! self fails? You already have committed your SubscriptionItem to the database.

You could change the callback to before_create :build_customer_quotas and in your CustomerQuotaService call find_or_initialize build instead of find_or_create.

This would ensure everything is in one transaction and would be properly saved or rollbacked.

Update:

Some code that might work:

class SubscriptionItem < ApplicationRecord
  before_create :build_customer_quotas

  def build_customer_quotas
    quotas.each do |quota|
      customer_quotas.build quota: quota, customer: customer
    end
  end
end

The customer_quotas will be saved automatically by rails when it saves the subscription_item itself. No extra save calls are needed.

Notes:

  • as build_customer_quotas is called before_create you don't need to check the existence of existing customer_quotas
  • you don't mention how you create the quotas that you are iterating from in your service. You can use a similar technique to build them and had them linked in your subcription_item object in memory.
Related