DEV Community

John Carneiro
John Carneiro

Posted on • Updated on

RSpec, use_transaction_fixtures and after_commit

Hello everyone, this is my first post on Dev.to, I want to use this post to share some knowledge and practice my skills as writer. Thank you for reading my post.

Firstly, some information about our project

We use a legacy project with Rails 4, RSpec (also FactoryGirl) with use_transaction_fixtures activated (see more details). This means that every example runs within a transaction, and then it removes that data by simply rolling back the transaction at the end of the example.

When we need to test after_commit callback we execute the method object.run_callbacks(:commit) after the operation (create, update or destroy).

What did I want to do?

I was writing a test for my model and there were three callbacks of type after_commit, one for each transaction type (create, update and destroy). I wanted to ensure that the system would generate the information correctly.

Now, imagine you create one record, then you update the same record and after you delete the record.

Below is an example of the code.

it 'notify service at every change' do
  total_jobs = NotifyWorker.jobs.count
  expect(total_jobs).to be_zero

  person = create(:person)
  person.run_callbacks(:commit)

  expect(NotifyWorker.jobs.count).to eq(total_jobs + 1)
  expect(NotifyWorker.jobs[0]['args']).to eq(
    [
      'create',
      {
        'id' => person.id,
        'name' => person.name
      }
    ]
  )

  person.update(name: 'test')
  person.run_callbacks(:commit)

  expect(NotifyWorker.jobs.count).to eq(total_jobs + 2)
  expect(NotifyWorker.jobs[1]['args']).to eq(
    [
      'update',
      {
        'id' => person.id,
        'name' => person.name
      }
    ]
  )

  person.destroy
  person.run_callbacks(:commit)

  expect(NotifyWorker.jobs.count).to eq(total_jobs + 3)
  expect(NotifyWorker.jobs[2]['args']).to eq(
    [
      'destroy',
      {
        'id' => person.id
      }
    ]
  )
end
Enter fullscreen mode Exit fullscreen mode

I expected the app to create three jobs in sequence, one for each transaction type, but that's not what happened.

In this case of the update it genetared a job of type "create" and when I deleted the record, it generated a job correctly, so the problem was in the update, but I didn't understand why.

I searched on google some information that might help and found a question on StackOverflow about the same problem I had.

After create the record (person = create(:person)), a instance of @_start_transaction_state is initialized, but never cleared. See more details
This variable is used to control which is the action in transaction.

So, I just had to clear the variable, for that you can use the clear_transaction_record_state method before executing after_commit, but this is a protected method, so you should use the send method, like that.

object.send(:clear_transaction_record_state)

Below you can see the final result.

it 'notify service at every change' do
  total_jobs = NotifyWorker.jobs.count
  expect(total_jobs).to be_zero

  person = create(:person)
  person.run_callbacks(:commit)

  expect(NotifyWorker.jobs.count).to eq(total_jobs + 1)
  expect(NotifyWorker.jobs[0]['args']).to eq(
    [
      'create',
      {
        'id' => person.id,
        'name' => person.name
      }
    ]
  )

  person.send(:clear_transaction_record_state)
  person.update(name: 'test')
  person.run_callbacks(:commit)

  expect(NotifyWorker.jobs.count).to eq(total_jobs + 2)
  expect(NotifyWorker.jobs[1]['args']).to eq(
    [
      'update',
      {
        'id' => person.id,
        'name' => person.name
      }
    ]
  )

  person.send(:clear_transaction_record_state)
  person.destroy
  person.run_callbacks(:commit)

  expect(NotifyWorker.jobs.count).to eq(total_jobs + 3)
  expect(NotifyWorker.jobs[2]['args']).to eq(
    [
      'destroy',
      {
        'id' => person.id
      }
    ]
  )
end
Enter fullscreen mode Exit fullscreen mode

Again thank you for reading my post and I see you later.

References:

Top comments (3)

Collapse
 
betiol profile image
Nikollas Betiol

Awesome post. Thanks for sharing.

Collapse
 
yoelbl profile image
yoelbl

This holds for apps before Rails 5 right? github.com/rails/rails/pull/18458/...

Collapse
 
johnwmcarneiro profile image
John Carneiro

Thank you, I believe you're right, unfortunelly I work in legacy project, I will change the post.