How to test before_update callback in rails model with Rspec and FactoryGirl?
Asked Answered
G

3

4

I am trying to test the before_update callback of the model bellow.

models/option.rb:

class Option < ApplicationRecord
  belongs_to :activity

  has_many :suboptions, class_name: "Option", foreign_key: "option_id"

  belongs_to :parent, class_name: "Option", optional: true, foreign_key: "option_id"

  accepts_nested_attributes_for :suboptions, allow_destroy: true,
    reject_if: ->(attrs) { attrs['name'].blank? }

  validates :name, presence: true

  before_create :set_defaults
  before_update :set_updates


  def set_defaults
    self.suboptions.each do |sbp|
      sbp.activity_id = self.activity_id
    end
  end

  def set_updates
    suboptions.each do |child|
      child.activity_id = self.activity_id
    end
  end
end

spec/models/option.rb:

require 'rails_helper'

RSpec.describe Option, type: :model do

  describe "Callbacks" do
    it "before_create" do
      suboption = create(:option)
      option = create(:option, suboptions:[suboption])
      option.run_callbacks(:create) {false}
      expect(option.suboptions.first.activity_id).to eq suboption.activity_id
    end

    it "before_update" do

    end
  end



end

I successfully tested the before_create callback (at least it gave me the correct result). But I don't know how to test the before_update callback. Is there a way to do it?

Gradate answered 11/1, 2017 at 4:42 Comment(0)
S
12

Warning: this answer is opinionated.

Test behavior, not implementation.

A callback is an implementation detail. Don't test it directly. Instead, pretend that you don't know how the model works internally, and test how it behaves.

If I'm reading the code correctly, the behavior can be described like so:

When updating an option, the activity_id of each of its suboptions is set to the activity_id of the option.

Create an option with suboptions. Update it, reload it, and check that the value of each activity_id is correct.

This will be slower than mocking, but less brittle. Also, the test is much easier to write and maintain.

Sevigny answered 12/1, 2017 at 2:37 Comment(0)
Z
7

Ok. I'll try to start from the beginning.

To test callback you have to test that it would be called when it should. That's all.

You may want to test the code of the method exactly. But such methods usually are private and they should be private indeed. And you shouldn't test private methods' code at all. If you want to do it anyway, your test will be coupled to your private methods and that's not good.

You can test before_update :set_updates like this:

let(:option) { Option.create("init your params here") }

it "test callback" do
  expect(option).to receive(:set_updates)
  option.save
end

If you want to test the code of your private method, you can do that like this

let(:option) { Option.create("init your params here") }

it "test callback" do
  # expect to receive some messages
  # which are in your method code
  # for example
  expect_any_instance_of(Suboption).to receive(:activity_id=)
  option.send(:set_updates)
end

P.S. You may want to watch/listen "Rails Conf 2013 The Magic Tricks of Testing by Sandi Metz". It's very helpful thing.

Zsigmondy answered 11/1, 2017 at 12:56 Comment(0)
G
2

I got a solution using run_callbacks. I created an option and a suboption. Then I updated the activity_id of the option and used option.run_callbacks(:update) {false} to run before_update callback (if I used {true}, it would run before_update and after_update callbacks):

it "before_update" do
    suboption = create(:option)
    option = create(:option, suboptions:[suboption])
    option.update(activity_id: 5)
    option.run_callbacks(:update) {false}
    expect(option.suboptions.first.activity_id).to eq option.activity_id
end

If I do not use the option.run_callbacks(:update) {false}, the expect expression gets a different activity_id for option and suboption. But by using the code like that, the test runs correctly and option and suboption have the same activity_id.

Gradate answered 12/1, 2017 at 4:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.