How to test model's callback method independently?
Asked Answered
C

6

42

I had a method in a model:

class Article < ActiveRecord::Base
  def do_something
  end
end

I also had a unit test for this method:

# spec/models/article_spec.rb
describe "#do_something" do
  @article = FactoryGirl.create(:article)
  it "should work as expected" do
    @article.do_something
    expect(@article).to have_something
  end
  # ...several other examples for different cases
end

Everything was fine until I found it's better to move this method into a after_save callback:

class Article < ActiveRecord::Base
  after_save :do_something

  def do_something
  end
end

Now all my tests about this method broken. I have to fix it by:

  • No more specific call to do_something because create or save will trigger this method as well, or I'll meet duplicate db actions.
  • Change create to build
  • Test respond_to
  • Use general model.save instead of individual method call model.do_something

    describe "#do_something" do
      @article = FactoryGirl.build(:article)
      it "should work as expected" do
        expect{@article.save}.not_to raise_error
        expect(@article).to have_something
        expect(@article).to respond_to(:do_something)
      end
    end
    

The test passed but my concern is it's no longer about the specific method. The effect will be mixed with other callbacks if more added.

My question is, is there any beautiful way to test model's instance methods independently that becoming a callback?

Cervical answered 21/5, 2013 at 19:12 Comment(2)
It is not clear why your original approach does not still work for testing. Unit test the method directly and just test that it is called as a callback independently. Am I missing something or is there something you don't like about that approach?Selfrestraint
@AndrewHubbs, thanks for your question. The reason is this method altered db. For example, it will assign this article to category "Rails". After refacoring to callback, when I call FactoryGirl.create, this callback will take effect and assign article to category "Rails". When I call this method again within test, there will be an error because it's already assigned.Cervical
T
90

Callback and Callback behavior are independent tests. If you want to check an after_save callback, you need to think of it as two things:

  1. Is the callback being fired for the right events?
  2. Is the called function doing the right thing?

Assume you have the Article class with many callbacks, this is how you would test:

class Article < ActiveRecord::Base
  after_save    :do_something
  after_destroy :do_something_else
  ...
end

it "triggers do_something on save" do
  expect(@article).to receive(:do_something)
  @article.save
end

it "triggers do_something_else on destroy" do
  expect(@article).to receive(:do_something_else)
  @article.destroy
end

it "#do_something should work as expected" do
  # Actual tests for do_something method
end

This decouples your callbacks from behavior. For example, you could trigger the same callback method article.do_something when some other related object is updated, say like user.before_save { user.article.do_something }. This will accomodate all those.

So, keep testing your methods as usual. Worry about the callbacks separately.

Edit: typos and potential misconceptions Edit: change "do something" to "trigger something"

Transpierce answered 21/5, 2013 at 19:43 Comment(3)
+1 for "decouples your callbacks from behavior." RDX, what is your recommendation for "# Actual tests for do_something method"? Use my method later(test overall behaviour by "save" the object)?Cervical
Just the actual tests for do_something method, somewhat like the first rspec in your post: @article.do_something; expect(@article).to have_something. Basically save is a larger method, which could potentially trigger a lot of before_saves, after_saves, etc. But if you have tested all the individual methods, and you know that every method is doing its job correctly, then you only need to write very less and short tests for the save/before_save/after_save/... methods. Compose bigger functions out of well tested smaller functions :)Transpierce
by chance, do you have any thoughts on #35950970Prendergast
Q
19

You can use shoulda-callback-matchers to test existence of your callbacks without calling them.

describe Article do
  it { is_expected.to callback(:do_something).after(:save) }
end

If you also want to test the behaviour of the callback:

describe Article do
  ...

  describe "#do_something" do
    it "gives the article something" do
      @article.save
      expect(@article).to have_something
    end
  end
end
Quarrel answered 28/8, 2014 at 15:56 Comment(0)
A
4

I like to use ActiveRecord #run_callbacks method to make sure callbacks are been called without need to hit database. This way it runs faster.

describe "#save" do
  let(:article) { FactoryBot.build(:article) }
  it "runs .do_something after save" do
    expect(article).to receive(:do_something)
    article.run_callbacks(:save)
  end
end

And to test the behavior of #do_something you add another test specifically for that.

describe "#do_something" do
  let(:article) { FactoryBot.build(:article) }
  it "return thing" do
    expect(article.do_something).to be_eq("thing")
  end
end
Amena answered 30/6, 2020 at 19:59 Comment(1)
run_callbacks only calls the before and around callbacks fwiwMeningitis
O
3

In the spirit of Sandi Metz and minimalist testing, the suggestion in https://mcmap.net/q/382230/-how-to-test-model-39-s-callback-method-independently to confirm the call to a possibly private method does not seem right to me.

Testing a publicly-observable side-effect or confirming an outgoing command message makes more sense to me. Christian Rolle provided an example at http://www.chrisrolle.com/en/blog/activerecord-callback-tests-with-rspec.

Ordinal answered 15/8, 2018 at 15:41 Comment(1)
Couldn't agree more. Thanks for sharing the article.Kelci
L
0

This is more of a comment than an answer, but I put it here for the syntax highlighting...

I wanted a way to skip the callbacks in my tests, this is what I did. (This might help with the tests that broke.)

class Article < ActiveRecord::Base
  attr_accessor :save_without_callbacks
  after_save :do_something

  def do_something_in_db
    unless self.save_without_callbacks
      # do something here
    end
  end
end

# spec/models/article_spec.rb
describe Article do
  context "after_save callback" do
    [true,false].each do |save_without_callbacks|
      context "with#{save_without_callbacks ? 'out' : nil} callbacks" do
        let(:article) do
          a = FactoryGirl.build(:article)
          a.save_without_callbacks = save_without_callbacks
        end
        it do
          if save_without_callbacks
            # do something in db
          else
            # don't do something in db
          end
        end
      end
    end
  end
end
Lens answered 21/5, 2013 at 19:30 Comment(3)
Teddy, thanks for your answer, or comment? :) I'm afraid the cost is a bit high by this way, with some extra code in test, and altering of model to adapt to test deliberately.Cervical
Yeah, I also had to seed the db, where skipping callbacks was a requirement, so I got more benefit in my case.Lens
There is a skip_callback method for this: github.com/rails/rails/blob/…Whenas
V
-1
describe "#do_something" do

 it "gives the article something" do

  @article = FactoryGirl.build(:article)

   expect(@article).to have_something

 @article.save
end

end
Vick answered 23/7, 2015 at 8:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.