I have the following to remove the spaces on a specific attribute.
#before_validation :strip_whitespace
protected
def strip_whitespace
self.title = self.title.strip
end
And I want to test it. For now, I've tried:
it "shouldn't create a new part with title beggining with space" do
#part = Part.new(#attr.merge(:title => " Test"))
#part.title.should.eql?("Test")
end
What am I missing?
Validations won't run until the object is saved, or you invoke valid? manually. Your before_validation callback isn't being run in your current example because your validations are never checked. In your test I would suggest that you run #part.valid? before checking that the title is changed to what you expect it to be.
app/models/part.rb
class Part < ActiveRecord::Base
before_validation :strip_whitespace
protected
def strip_whitespace
self.title = self.title.strip
end
end
spec/models/part_spec.rb
require 'spec_helper'
describe Part do
it "should remove extra space when validated" do
part = Part.new(:title => " Test")
part.valid?
part.title.should == "Test"
end
end
This will pass when the validation is included, and fails when the validation is commented out.
referring to #danivovich example
class Part < ActiveRecord::Base
before_validation :strip_whitespace
protected
def strip_whitespace
self.title = self.title.strip
end
end
the proper way to write the spec is to separately write spec on strip_whitespace method and then just check if model class have callback set on it, like this:.
describe Part do
let(:record){ described_class.new }
it{ described_class._validation_callbacks.select{|cb| cb.kind.eql?(:before)}.collect(&:filter).should include(:strip_whitespace) }
#private methods
describe :strip_whitespace do
subject{ record.send(:strip_whitespace)} # I'm using send() because calling private method
before{ record.stub(:title).and_return(' foo ')
it "should strip white spaces" do
subject.should eq 'foo'
# or even shorter
should eq 'foo'
end
end
end
if you need to skip callback behavior in some scenarios use
before{ Part.skip_callback(:validation, :before, :strip_whitespace)}
before{ Part.set_callback( :validation, :before, :strip_whitespace)}
Update 20.01.2013
BTW I wrote a gem with RSpec matchers to test this https://github.com/equivalent/shoulda-matchers-callbacks
In general I don't recommend callback at all. They are fine for example in this question, however once you do more complicated stuff like:
After create->
link account to user
create notification
send email to Admin
...then you should create custom service object to deal with this and test them separately.
http://railscasts.com/episodes/398-service-objects
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
Related
I'm trying to check if a method isn't invoked within an after_update callback.
I'm currently doing:
class Foo
def self.call; end
end
def Model < ActiveRecord::Base
after_update :do_something
def do_something
return unless title_changed?
Foo.call
end
end
In the test it works like that:
test 'Foo.new.bar is invoked' do
mock = Minitest::mock.new
mock.expect :call, nil
Foo.stub(:call) { update_record_to_fire_callback }
mock.verify
end
And it beautifully pass, but now I'm trying to do the opposite but without luck as I don't know how to do it. If I do assert_not mock.verify Minitest complains anyway for the method to be executed.
Or maybe there are other way to check a method isn't invoked? The method will do an expensive request, so, I want to avoid that.
I'm using Rails 5 and sadly Minitest. I'm open to add any gem that can work with these versions.
Since you're open to adding a gem, mocha works well for this. Add the gem, then use mocha's Expectation#never. Your test can then look like:
test 'Foo.new.bar is not invoked' do
model = Model.new
Foo.expects(:call).never
model.update!(not_the_title: 'value')
end
The easiest way to accomplish this is to stub the method that you want to ensure isn't called and have it raise an error.
class ModelTest < ActiveSupport::TestCase
test "does not call Foo.call when title is not changed" do
model = Model.new
refute model.title_changed?
Foo.stub(:call, -> { raise "Foo was called!" }) do
model.title = "something new"
end
assert model.title_changed?
end
end
There is no assertion to check that an error was not explicitly raised. Rails does have assert_nothing_raised, but Minitest does not. You can add it if you like.
class ModelTest < ActiveSupport::TestCase
test "does not call Foo.call when title is not changed" do
model = Model.new
refute model.title_changed?
Foo.stub(:call, -> { raise "Foo was called!" }) do
assert_nothing_raised do
model.title = "something new"
end
end
assert model.title_changed?
end
end
I'm trying to test to make sure the notification mailer is not sending after an invalid record but I keep getting below error before test can complete
"ActiveRecord::RecordInvalid:
it 'does not call send_email_notification' do
expect(NotificationMailer).not_to receive(:user_notification)
FactoryGirl.create(:invalid_user, shop: shop)
end
How can I test this properly?
EDIT: here's the code where the mail gets sent:
after_create :send_email_notification
private
def send_email_notification
if self.shop.email_notifications
NotificationMailer.user_notification(self).deliver_now
end
end
end
it 'does not send notification email when user is invalid' do
expect(NotificationMailer).not_to receive(:user_notification)
post :create, user: attributes_for(:invalid_user)
end
So, what this is doing is set you're expectation just the way you did, and then post to the user_controller create method the invalid_user attributes.
Of course, the post shouldn't be allowed to create the record if you have set your validations correctly in the user model, and subsequently not call NotificationMailer.user_notification.
Note that attributes_for is another FactoryGirl method that you can use to arrange and pass your factory attributes as controller params.
Now! why does it not work with your original approach?
It is is because FactoryGirl is complaining that it cannot create the record, and that is absolutely logical since you're trying to create an invalid user. The failing error has nothing to do with testing your email notification, but rather with the way you setup your Factory.
Final note! If you run the test and it complains:
"NoMethodError: undefined method `post' for #<RSpec::ExampleGroups"
It probably means that your spec file is not located under spec/controllers.
post, create, patch, delete methods are part of the RSpec::Rails::ControllerExampleGroup
To solve this please refer to the following Stackoverflow answer
Hope this helps.
Below is some code I used to test your use case: you can copy and paste it into a file and run rspec on it. I hope that the assumptions I made about the parts of your Rails app you didn't disclose aren't too far off the mark.
require 'active_record'
require 'factory_girl'
require 'rspec'
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3', database: ':memory:'
)
class User < ActiveRecord::Base
has_one :shop
validates :email, presence: true
after_create :send_notification
private
def send_notification
if shop.email_notifications
NotificationMailer.user_notification(self).deliver_now
end
end
end
class Shop < ActiveRecord::Base
belongs_to :user
end
ActiveRecord::Schema.define do
create_table :users do |t|
t.string :email
end
create_table :shops do |t|
t.boolean :email_notifications
t.belongs_to :user
end
end
FactoryGirl.define do
factory :user do
email "test#example.com"
shop
factory :invalid_user do
email nil
end
end
factory :shop do
email_notifications true
end
end
RSpec.describe User do
context 'on save' do
let(:mailer) { double('mailer') }
before do
# You probably won't need this stub_const since the class will exist
# in your app
stub_const('NotificationMailer', Class.new)
allow(NotificationMailer).to \
receive(:user_notification).with(user).and_return(mailer)
end
context 'when user is valid' do
let(:user) { FactoryGirl.build(:user) }
it 'calls to send email notifications' do
expect(mailer).to receive(:deliver_now)
user.save
end
end
context 'when user is invalid' do
let(:user) { FactoryGirl.build(:invalid_user) }
it 'does not call to send email notifications' do
expect(mailer).to_not receive(:deliver_now)
user.save
end
end
end
end
Since you've got an external dependency in your callback (the callout to the separate class NotificationMailer), you may need to stub out messages to that dependency in order to make the test pass, otherwise you could get nil values returned when you likely don't expect them (see this blog post for more information).
Just an opinion, but you might even do future-you a favour if you only use callbacks when there are no external dependencies in them and the logic only refers to state internal to the object (User in this case). The change you would make would be something like moving the NotificationMailer.user_notification(self).deliver_now call out of the User model callback and into the controller where (I assume) you're making a call to save the user. The extraction might look something like:
def create
#user = User.new(user_params)
if #user.save
NotificationMailer.user_notification(#user).deliver_now
# do other stuff, render, redirect etc
else
# do something else
end
end
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?
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"
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
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
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.
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
describe "#do_something" do
it "gives the article something" do
#article = FactoryGirl.build(:article)
expect(#article).to have_something
#article.save
end
end
class TestController < AplicationController
#....
private
def some_method
unless #my_variable.nil?
#...
return true
end
end
end
I want to test some_method directly in controller spec:
require 'spec_helper'
describe TestController do
it "test some_method"
phone = Phone.new(...)
controller.assign(:my_variable,phone) #does not work
controller.send(:some_method).should be_true
end
end
How I can set TestController instance variable #my_variable from controller spec?
When testing private methods in controllers, rather than use send, I tend to use an anonymous controller due to not wanting to call the private method directly, but the interface to the private method (or, in the test below, effectively stubbing that interface). So, in your case, perhaps something like:
require 'spec_helper'
describe TestController do
controller do
def test_some_method
some_method
end
end
describe "a phone test with some_method" do
subject { controller.test_some_method }
context "when my_variable is not nil" do
before { controller.instance_variable_set(:#my_variable, Phone.new(...)) }
it { should be_true }
end
context "when my_variable is nil" do
before { controller.instance_variable_set(:#my_variable, nil) }
it { should_not be_true } # or should be_false or whatever
end
end
end
There's some good discussion on the issue of directly testing private methods in this StackOverflow Q&A, which swayed me towards using anonymous controllers, but your opinion may differ.
instance_eval is a relatively clean way to accomplish this:
describe TestController do
it "test some_method" do
phone = Phone.new(...)
controller.instance_eval do
#my_variable = phone
end
controller.send(:some_method).should be_true
end
end
In this case, using do...end on instance_eval is overkill, and those three lines can be shortened to:
controller.instance_eval {#my_variable = phone}
I don't think you want to access an instance variable from your spec controller, as the spec should test the behaviour, but you can always stub the private method.
In your case it should be something like this (in this example it doesn't make so much sense):
describe TestController do
it "test some_method"
phone = Phone.new(...)
controller.stub(:some_method).and_return(true)
controller.send(:some_method).should be_true
end
end
If this is not what you are looking for take a look at this: How to set private instance variable used within a method test?
I've got a model class that overrides update_attributes:
class Foo < ActiveRecord::Base
def update_attributes(attributes)
if super(attributes)
#do some other cool stuff
end
end
end
I'm trying to figure out how to set an expectation and/or stub on the super version of update_attributes to make sure that in the success case the other stuff is done. Also I want to make sure that the super method is actually being called at all.
Here's what I have tried so far (and it didn't work, of course):
describe "#update_attributes override" do
it "calls the base class version" do
parameters = Factory.attributes_for(:foo)
foo = Factory(:foo, :title => "old title")
ActiveRecord::Base.should_receive(:update_attributes).once
foo.update_attributes(parameters)
end
end
This doesn't work, of course:
Failure/Error: ActiveRecord::Base.should_recieve(:update_attributes).once
NoMethodError:
undefined method `should_recieve' for ActiveRecord::Base:Class
Any ideas?
update_attributes is an instance method, not a class method, so you cannot stub it directly on ActiveRecord::Base with rspec-mocks, as far as I know. And I don't think that you should: the use of super is an implementation detail that you shouldn't be coupling your test to. Instead, its better to write examples that specify the behavior you want to achieve. What behavior do you get from using super that you wouldn't get if super wasn't used?
As an example, if this was the code:
class Foo < ActiveRecord::Base
def update_attributes(attributes)
if super(attributes)
MyMailer.deliver_notification_email
end
end
end
...then I think the interesting pertinent behavior is that the email is only delivered if there are no validation errors (since that will cause super to return true rather than false). So, I might spec this behavior like so:
describe Foo do
describe "#update_attributes" do
it 'sends an email when it passes validations' do
record = Foo.new
record.stub(:valid? => true)
MyMailer.should_receive(:deliver_notification_email)
record.update_attributes(:some => 'attribute')
end
it 'does not sent an email when it fails validations' do
record = Foo.new
record.stub(:valid? => false)
MyMailer.should_receive(:deliver_notification_email)
record.update_attributes(:some => 'attribute')
end
end
end
Try replacing should_recieve with should_receive.