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.
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.
Actually, I want to test a model callback.
system_details.rb (Model)
class SystemDetail < ActiveRecord::Base
belongs_to :user
attr_accessible :user_agent
before_create :prevent_if_same
def agent
Browser.new(ua: user_agent, accept_language: 'en-us')
end
def prevent_if_same
rec = user.system_details.order('updated_at desc').first
return true unless rec
if rec.user_agent == user_agent
rec.touch
return false
end
end
end
prevent_if_same method is working fine and working as expected but it raises exception ActiveRecord::RecordNotSaved when it returns false, and the exception breaks the rspec test.what I want is, It should silently cancel save without raising an exception.
system_detail_spec.rb (rspec)
require 'rails_helper'
RSpec.describe SystemDetail, :type => :model do
context '#agent' do
it 'Checks browser instance' do
expect(SystemDetail.new.agent).to be_an_instance_of(Browser)
end
end
context '#callback' do
it 'Ensure not creating consecutive duplicate records' do
user = create :end_user
system_detail = create :system_detail, :similar_agent, user_id: user.id
updated_at = system_detail.updated_at
system_detail2 = create :system_detail, :similar_agent, user_id: user.id
system_detail.reload
expect(system_detail2.id).to be_nil
expect(system_detail.updated_at).to be > updated_at
end
end
end
The 2nd test #callback was failing because of exception.
Failures:
1) SystemDetail#callback ensure not creating duplicate records
Failure/Error: system_detail2 = create :system_detail, :similar_agent, user_id: user.id
ActiveRecord::RecordNotSaved:
ActiveRecord::RecordNotSaved
Is there any way to silently cancel save without raising an exception?
I think in this case is better to use validate :prevent_if_same because basically your method is kind of validation rule. Also it would prevent from creation silently. You can always add error if you want to notify user about unsuccessful creation
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
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.
Provided that I have a project factory
Factory.define :project do |p|
p.sequence(:title) { |n| "project #{n} title" }
p.sequence(:subtitle) { |n| "project #{n} subtitle" }
p.sequence(:image) { |n| "../images/content/projects/#{n}.jpg" }
p.sequence(:date) { |n| n.weeks.ago.to_date }
end
And that I'm creating instances of project
Factory.build :project
Factory.build :project
By this time, the next time I execute Factory.build(:project) I'll receive an instance of Project with a title set to "project 3 title" and so on. Not surprising.
Now say that I wish to reset my counter within this scope. Something like:
Factory.build :project #=> Project 3
Factory.reset :project #=> project factory counter gets reseted
Factory.build :project #=> A new instance of project 1
What would be the best way to achieve this?
I'm currently using the following versions:
factory_girl (1.3.1)
factory_girl_rails (1.0)
Just call FactoryGirl.reload in your before/after callback. This is defined in the FactoryGirl codebase as:
module FactoryGirl
def self.reload
self.factories.clear
self.sequences.clear
self.traits.clear
self.find_definitions
end
end
Calling FactoryGirl.sequences.clear is not sufficient for some reason. Doing a full reload might have some overhead, but when I tried with/without the callback, my tests took around 30 seconds to run either way. Therefore the overhead is not enough to impact my workflow.
After tracing my way through the source code, I have finally come up with a solution for this. If you're using factory_girl 1.3.2 (which was the latest release at the time I am writing this), you can add the following code to the top of your factories.rb file:
class Factory
def self.reset_sequences
Factory.factories.each do |name, factory|
factory.sequences.each do |name, sequence|
sequence.reset
end
end
end
def sequences
#sequences
end
def sequence(name, &block)
s = Sequence.new(&block)
#sequences ||= {}
#sequences[name] = s
add_attribute(name) { s.next }
end
def reset_sequence(name)
#sequences[name].reset
end
class Sequence
def reset
#value = 0
end
end
end
Then, in Cucumber's env.rb, simply add:
After do
Factory.reset_sequences
end
I'd assume if you run into the same problem in your rspec tests, you could use rspecs after :each method.
At the moment, this approach only takes into consideration sequences defined within a factory, such as:
Factory.define :specialty do |f|
f.sequence(:title) { |n| "Test Specialty #{n}"}
f.sequence(:permalink) { |n| "permalink#{n}" }
end
I have not yet written the code to handle: Factory.sequence...
There is a class method called sequence_by_name to fetch a sequence by name, and then you can call rewind and it'll reset to 1.
FactoryBot.sequence_by_name(:order).rewind
Or if you want to reset all.
FactoryBot.rewind_sequences
Here is the link to the file on github
For googling people: without further extending, just do FactoryGirl.reload
FactoryGirl.create :user
#=> User id: 1, name: "user_1"
FactoryGirl.create :user
#=> User id: 2, name: "user_2"
DatabaseCleaner.clean_with :truncation #wiping out database with truncation
FactoryGirl.reload
FactoryGirl.create :user
#=> User id: 1, name: "user_1"
works for me on
* factory_girl (4.3.0)
* factory_girl_rails (4.3.0)
https://stackoverflow.com/a/16048658
According to ThoughBot Here, the need to reset the sequence between tests is an anti-pattern.
To summerize:
If you have something like this:
FactoryGirl.define do
factory :category do
sequence(:name) {|n| "Category #{n}" }
end
end
Your tests should look like this:
Scenario: Create a post under a category
Given a category exists with a name of "My Category"
And I am signed in as an admin
When I go to create a new post
And I select "My Category" from "Categories"
And I press "Create"
And I go to view all posts
Then I should see a post with the category "My Category"
Not This:
Scenario: Create a post under a category
Given a category exists
And I am signed in as an admin
When I go to create a new post
And I select "Category 1" from "Categories"
And I press "Create"
And I go to view all posts
Then I should see a post with the category "Category 1"
Had to ensure sequences are going from 1 to 8 and restart to 1 and so on. Implemented like this:
class FGCustomSequence
def initialize(max)
#marker, #max = 1, max
end
def next
#marker = (#marker >= #max ? 1 : (#marker + 1))
end
def peek
#marker.to_s
end
end
FactoryGirl.define do
factory :image do
sequence(:picture, FGCustomSequence.new(8)) { |n| "image#{n.to_s}.png" }
end
end
The doc says "The value just needs to support the #next method." But to keep you CustomSequence object going through it needs to support #peek method too. Lastly I don't know how long this will work because it kind of hack into FactoryGirl internals, when they make a change this may fail to work properly
There's no built in way to reset a sequence, see the source code here:
http://github.com/thoughtbot/factory_girl/blob/master/lib/factory_girl/sequence.rb
However, some people have hacked/monkey-patched this feature in. Here's an example:
http://www.pmamediagroup.com/2009/05/smarter-sequencing-in-factory-girl/
To reset particular sequence you can try
# spec/factories/schedule_positions.rb
FactoryGirl.define do
sequence :position do |n|
n
end
factory :schedule_position do
position
position_date Date.today
...
end
end
# spec/models/schedule_position.rb
require 'spec_helper'
describe SchedulePosition do
describe "Reposition" do
before(:each) do
nullify_position
FactoryGirl.create_list(:schedule_position, 10)
end
end
protected
def nullify_position
position = FactoryGirl.sequences.find(:position)
position.instance_variable_set :#value, FactoryGirl::Sequence::EnumeratorAdapter.new(1)
end
end
If you are using Cucumber you can add this to a step definition:
Given(/^I reload FactoryGirl/) do
FactoryGirl.reload
end
Then just call it when needed.