How to test model's callback method independently?
Solution 1
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:
- Is the callback being fired for the right events?
- 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"
Solution 2
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
Solution 3
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
Solution 4
In the spirit of Sandi Metz and minimalist testing, the suggestion in https://stackoverflow.com/a/16678194/2001785 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.
Billy Chan
Twitter: http://www.twitter.com/billy_as_coder Github: https://github.com/billychan
Updated on July 01, 2020Comments
-
Billy Chan almost 4 years
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
becausecreate
orsave
will trigger this method as well, or I'll meet duplicate db actions. - Change
create
tobuild
- Test respond_to
Use general
model.save
instead of individual method callmodel.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?
- No more specific call to