I have a Reminder model that needs to calculate an important piece of data upon creation. I'm using the before_create callback for this purpose:
class Reminder < ActiveRecord::Base
validates :next_send_time, presence: true
before_create :set_next_send_time
def set_next_send_time
self.next_send_time = calc_next_send_time
end
end
The problem is, the callback doesn't seem to be running in my controller spec. The attribute is vital to the model and throughout the rest of my application's tests I will expect it to be calculated upon create.
The reminders_controller#create method is
def create
respond_with Reminder.create(reminder_params)
end
and here's the reminders_controller_spec:
RSpec.describe Api::RemindersController do
describe 'create' do
it "should work" do
post :create,
reminder: {
action: 'Call mom',
frequency: 1,
type: 'WeeklyReminder',
day_of_week: 0,
start_date: Date.new,
time_of_day: '07:00:00',
user_id: 1
},
format: :json
reminders = Reminder.all
expect(reminders.length).to eq(1)
end
end
end
If I inspect the response, the error is that next_send_time is null.
How can I get the Reminder's callback to run in my Rspec tests?
Instead of before_create, try before_validation :set_next_time, on: :create.
If the record doesn't pass validation, the callback wouldn't fire.
EDIT: Corrected the method name to before_validation.
If you're using Rails 4.0 you can use the [TestAfterCommit][1] gem.
If you're using Rails 5.0 it is built in: https://github.com/rails/rails/pull/18458
Related
I have a model 'Policy'. Within that model, I have presence validations for policy_holder and premium_amount. I'm attempting to write a MiniTest test for this model. For some reason, my tests are failing.
Here is my model:
class Policy < ApplicationRecord
belongs_to :industry
belongs_to :carrier
belongs_to :agent
validates :policy_holder, presence: true
validates :premium_amount, presence: true
end
And here are my tests:
require 'test_helper'
class PolicyTest < ActiveSupport::TestCase
test 'should validate policy holder is present' do
policy = Policy.find_or_create_by(policy_holder: nil, premium_amount: '123.45',
industry_id: 1, carrier_id: 1,
agent_id: 1)
assert_not policy.valid?
end
test 'should validate premium amount is present' do
policy = Policy.find_or_create_by(policy_holder: 'Bob Stevens', premium_amount: nil,
industry_id: 1, carrier_id: 1,
agent_id: 1)
assert_not policy.valid?
end
test 'should be valid when both policy holder and premium amount are present' do
policy = Policy.find_or_create_by(policy_holder: 'Bob Stevens', premium_amount: '123.45',
industry_id: 1, carrier_id: 1,
agent_id: 1)
assert policy.valid?
end
end
Here is the failure message:
Failure:
PolicyTest#test_should_be_valid_when_both_policy_holder_and_premium_amount_are_present [test/models/policy_test.rb:22]:
Expected false to be truthy.
The last test is failing when I believe is should be passing. This has me thinking that my other tests are not correct either.
There is a much easier way to test validations with less "carpet bombing" involved:
require 'test_helper'
class PolicyTest < ActiveSupport::TestCase
setup do
#policy = Policy.new
end
test "should validate presence of policy holder" do
#policy.valid? # triggers the validations
assert_includes(
#policy.errors.details[:policy_holder],
{ error: :blank }
)
end
# ...
end
This tests just that validation and not every validation on the model combined. Using assert policy.valid? will not tell you anything about what failed in the error message.
errors.details was added in Rails 5. In older versions you need to use:
assert_includes( policy.errors[:premium_amount], "can't be blank" )
Which tests against the actual error message. Or you can use active_model-errors_details which backports the feature.
So what's happening here is the validations are failing on the model.
.valid? will return true if there are no errors on the object when the validations are run.
Since you are clearly seeing a "false", that means one or more of the validations on the model are failing.
In a Rails console, you should try creating an object manually and casting it to a variable, then testing it to see the errors thusly:
test = Policy.new(whatever params are needed to initialize here)
# This will give you the object
test.valid?
#This will likely return FALSE, and NOW you can run:
test.errors
#This will actually show you where the validation failed inside the object
Regardless, this is almost assuredly a problem in the model and its creation.
Keep in mind, .errors won't work until AFTER you run .valid? on the object.
Sorry for the vague title, there are a lot of moving parts to this problem so I think it will only be clear after seeing my code. I'm fairly sure I know what's going on here and am looking for feedback on how to do it differently:
I have a User model that sets a uuid attr via an ActiveRecord callback (this is actually in a "SetsUuid" concern, but the effect is this):
class User < ActiveRecord::Base
before_validation :set_uuid, on: :create
validates :uuid, presence: true, uniqueness: true
private
def set_uuid
self.uuid = SecureRandom.uuid
end
end
I am writing a functional rspec controller test for a "foo/add_user" endpoint. The controller code looks like this (there's some other stuff like error-handling and #foo and #params being set by filters, but you get the point. I know this is all working.)
class FoosController < ApplicationController
def add_user
#foo.users << User.find_by_uuid!(#params[:user_id])
render json: {
status: 'awesome controller great job'
}
end
end
I am writing a functional rspec controller test for the case "foo/add_user adds user to foo". My test looks roughly this (again, leaving stuff out here, but the point should be obvious, and I know it's all working as intended. Also, just to preempt the comments: no, I'm not actually 'hardcoding' the "user-uuid" string value in the test, this is just for the example)
RSpec.describe FoosController, type: :controller do
describe '#add_user' do
it_behaves_like 'has #foo' do
it_behaves_like 'has #params', {user_id: 'user-uuid'} do
context 'user with uuid exists' do
let(:user) { create(:user_with_uuid, uuid: params[:user_id]) } # params is set by the 'has #params' shared_context
it 'adds user with uuid to #foo' do
route.call() # route is defined by a previous let that I truncated from this example code
expect(foo.users).to include(user) # foo is set by the 'has #foo' shared_context
end
end
end
end
end
end
And here is my user factory (I've tried setting the uuid in several different ways, but my problem (that I go into below) is always the same. I think this way (with traits) is the most elegant, so that's what I'm putting here):
FactoryGirl.define do
factory :user do
email { |n| "user-#{n}#example.com" }
first_name 'john'
last_name 'naglick'
phone '718-555-1234'
trait :with_uuid do
after(:create) do |user, eval|
user.update!(uuid: eval.uuid)
end
end
factory :user_with_uuid, traits: [:with_uuid]
end
end
Finally, The problem:
This only works if I reference user.uuid before route.call() in the spec.
As in, if I simply add the line "user.uuid" before route.call(), everything works as intended.
If I don't have that line, the spec fails because the user's uuid doesn't actually get updated by the after(:create) callback in the trait in the factory, and thus the User.find_by_uuid! line in the controller does not find the user.
And just to preempt another comment: I'm NOT asking how to re-write this spec so that it works like I want. I already know a myriad of ways to do this (the easiest and most obvious being to manually update user.uuid in the spec itself and forget about setting the uuid in the factory altogether). The thing I'm asking here is why is factorygirl behaving like this?
I know it has something to do with lazy-attributes (obvious by the fact it magically works if I have a line evaluating user.uuid), but why? And, even better: is there some way I can do what I want here (setting the uuid in the factory) and have everything work like I intend? I think it's a rather elegant looking use of rspec/factorygirl, so I'd really like it to work like this.
Thanks for reading my long question! Very much appreciate any insight
Your issue has less to do with FactoryGirl and more to do with let being lazily evaluated.
From the docs:
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first time
the method it defines is invoked. You can use let! to force the method's
invocation before each example.
Since your test doesn't invoke the user object until the expectation there is nothing created. To force rspec to load object, you can use let!.
Instead of using the before_validation callback you should be using after_initialize. That way the callback is fired even before .valid? is called in the model lifecycle.
class User < ActiveRecord::Base
before_initialization :set_uuid!, on: :create, if: :set_uuid?
validates :uuid, presence: true, uniqueness: true
private
def set_uuid!
# we should also check that the UUID
# does not actually previously exist in the DB
begin
self.uuid = SecureRandom.uuid
end while User.where(uuid: self.uuid).any?
end
def set_uuid?
self.uuid.nil?
end
end
Although the chance of generating the same hash twice with SecureRandom.uuid is extremely slim it is possible due to the pigeonhole principle. If you maxed out in the bad luck lottery this would simply generate a new UUID.
Since the callback fires before validation occurs the actual logic here should be completely self contained in the model. Therefore there is no need to setup a callback in FactoryGirl.
Instead you would setup your spec like so:
let!(:user) { create(:user) }
it 'adds user with uuid to #foo' do
post :add_user, user_id: user.uuid, { baz: 3 }
end
I'm getting intermittent test failures when using instance_double.
I have a file with 4 specs in it. Here is the source:
require 'rails_helper'
describe SubmitPost do
before(:each) do
#post = instance_double('Post')
allow(#post).to receive(:submitted_at=)
end
context 'on success' do
before(:each) do
allow(#post).to receive(:save).and_return(true)
#result = SubmitPost.call(post: #post)
end
it 'should set the submitted_at date' do
expect(#post).to have_received(:submitted_at=)
end
it 'should call save' do
expect(#post).to have_received(:save)
end
it 'should return success' do
expect(#result.success?).to eq(true)
expect(#result.failure?).to eq(false)
end
end
context 'on failure' do
before(:each) do
allow(#post).to receive(:save).and_return(false)
#result = SubmitPost.call(post: #post)
end
it 'should return failure' do
expect(#result.success?).to eq(false)
expect(#result.failure?).to eq(true)
end
end
end
This is a Rails 4.1.4 application. Internally, SubmitPost sets submitted_at and calls save on the passed-in Post. My Post model looks like this:
class Post < ActiveRecord::Base
validates :title, presence: true
validates :summary, presence: true
validates :url, presence: true
validates :submitted_at, presence: true
scope :chronological, -> { order('submitted_at desc') }
end
It's super vanilla.
When I run rake, rspec, or bin/rspec, I get all all four tests failing 20% - 30% of the time. The error message is always:
Failure/Error: allow(#post).to receive(:submitted_at=)
Post does not implement: submitted_at=
If I label one of the specs with focus: true, that one spec will fail 100% of the time.
If I replace instance_double with double, all specs will succeed 100% of the time.
It appears that instance_double is having some difficulty inferring the methods available on the Post class. It also appears to be somewhat random and timing-based.
Has anyone run into this issue? Any ideas what might be wrong? Any sense of how to go about troubleshooting this? Naturally, inserting a debugging breakpoint causes the specs to pass 100% of the time.
The problem you are seeing is that ActiveRecord creates column methods dynamically. instance_double uses 'Post' to look up methods to verify you are stubbing them correctly (unless the class doesn't exist yet or has not been loaded).
When a prior spec loads the model, ActiveRecord will create those dynamic methods so your spec passes as RSpec can then find the methods (with a respond_to? call). When run in isolation the model hasn't been previously used and so ActiveRecord will not have created the dynamic methods yet and your test fails as you're experiencing.
The workaround for this is to force ActiveRecord to load the dynamic methods when they are called in your spec:
class Post < ActiveRecord::Base
def submitted_at=(value)
super
end
end
See the RSpec documentation for further explanation and workarounds for the problem:
https://www.relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/dynamic-classes
I have a Person class which HABTM Preferences - when a preference is added or removed I need to call a method which notifies a third-party API.
Right now my person class looks like this:
class Person < ActiveRecord::Base
has_and_belongs_to_many :preferences, :after_add => :send_communication_preference_update
def send_communication_preference_update(preference)
...
end
end
To test this I have the following spec:
describe 'person.preferences #after_add' do
let(:person) { FactoryGirl.create(:person) }
let(:pref) { [Preference.find_by_preference_name("EmailMarketing")] }
it 'should trigger callback' do
person.preferences = pref
person.should_receive(:send_communication_preference_update).with(pref.first)
end
end
However this does not work.
Even losing with(pref.first) results in the same error below.
The error I'm getting is:
Failure/Error: person.should_receive(:send_communication_preference_update).with(pref.first)
(#<Person:0x000000086297a8>).send_communication_preference_update(#<Preference preference_id: 4, preference_name: "EmailMarketing", created_at: "2014-07-08 08:31:23", updated_at: "2014-07-08 08:31:23", active: true, default_value: false>)
expected: 1 time
received: 0 times
Why is this?
change lines order in your specs: you should place should_receive before calling assigning
it 'should trigger callback' do
person.should_receive(:send_communication_preference_update).with(pref.first)
person.preferences = pref
end
You have to set up the should_receive before the method you want to test is called.
In my app, when a User is initialized, I want them to build 5 items. I've seen tests that assert there are, for example, expect(#user.items.count).to eq(5). However, I've been trying to validate the length of items and test the validation itself, not the number of objects associated with a user. Is this even possible? If so, what's the best way of going about this?
Here is the relevant code I have so far.
class User < ActiveRecord::Base
ITEMS_ALLOWED = 5
has_many :items
validates :items, length: {is: ITEMS_ALLOWED}
after_initialize :init_items
def init_items
ITEMS_ALLOWED.times { items.build }
end
...
My relevant test, using RSpec, Faker and FactoryGirl
describe User do
before :each do
#user = build(:user, username: "bob")
#user.save
end
it "is invalid with more than 5 items" do
user3 = build(:user)
user3.save
expect(user3.items.create(name: "test")).not_to be_valid
end
end
Currently the test tries to validate the item that's created. I tried to move the validation to the Item class instead, but I'm receiving the error, undefined method items for nil on the line that tries to call user.items.count.
class Item < ActiveRecord::Base
belongs_to :user
validates :number_of_items, length: {is: 5}
def number_of_items
errors.add("User must have exactly 5 items.") unless user.items.count == 5
end
end
================
Update: Failure Message when there are no validations in the Item class.
Failures:
1) User initialization is invalid with more than 5 items
Failure/Error: expect(user3.items.create(name: "test")).not_to be_valid
expected #<Item id: 16, name: "test", user_id: 3, photo: nil, created_at: "2014-01-14 00:24:11", updated_at: "2014-01-14 00:24:11", photo_file_name: nil, photo_content_type: nil, photo_file_size: nil, photo_updated_at: nil, description: nil> not to be valid
When you create your User instance, the init_items is being called and the Item instances are being created. However, the user's id is not defined at that point, so the user_id value of the created items is nil. This in turn results in the table's user method returning nil in your number_of_items validation.
When you remove the Item validations, then you're RSpec example will fail because you're doing a validation on an Item (i.e. the result of user3.items.create) rather than validating the resulting User. Instead, you can do something like this:
user3.items.create(name: "test")
expect(user3).to_not be_valid
I'd avoid using after_initialize. It is called whenever an object is instantiated, even after merely calling User.find. If you must use it, add a test for new_record? so that the items are only added for new User's.
An alternative approach is to write a builder method to use instead of User.new.
class User < ActiveRecord::Baae
ITEMS_ALLOWED = 5
has_many :items
validates :items, length { is: ITEMS_ALLOWED }
def self.build_with_items
new.tap do |user|
user.init_items
end
end
def init_items
ITEMS_ALLOWED.times { items.build }
end
end
describe User do
context "when built without items" do
let(:user) { User.new }
it "is invalid" do
expect(user.items.size).to eq 0
expect(user.valid?).to be_false
end
end
context "when built with items" do
let(:user) { User.build_with_items }
it "is valid" do
expect(user.items.size).to eq 5
expect(user.valid?).to be_true
end
end
end
This allows you to separate the item initialization from the user initialization, in case you end up wanting to have a User without items. In my experience, this works out better than requiring all newed up objects to be built the same way. The tradeoff is that you now need to use User.build_with_items in the new action in the controller.