Rspec mongoid - Testing embedded document callback (after_save) - ruby

I'm trying to create a test to check if my post's embedded document(author) call to it callback method.
Code:
class Post
include Mongoid::Document
include Mongoid::Timestamps::Created
include Mongoid::Timestamps::Updated
{....}
# relations
embeds_one :author, cascade_callbacks: true
accepts_nested_attributes_for :author
{...}
end
Class Author
include Mongoid::Document
include Mongoid::Timestamps::Created
include Mongoid::Timestamps::Updated
{...}
embedded_in :post
after_save :my_callback_method
def save_estimation_logs
{...}
end
{...}
end
test:
RSpec.describe Author, :type => :model do
context "Create author on Post" do
let!(:post) { create(:post, :with_external_author) }
it "should call after_save method my_callback_method when saving" do
expect(post.author).to receive(:my_callback_method)
expect(post.save).to eq true
end
end
end
when i'm trying to run this rspec - i got
Failure/Error: expect(post.author).to receive(:my_callback_method)
(#<Author _id: 5c7ea762f325709edac2ae84, created_at: 2019-03-05 16:44:18 UTC, updated_at: 2019-03-05 16:44:18 UTC>). my_callback_method(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Can you guys help me understand how should I test this embedded document callbacks?

First of all, you should trust mongoid to call after_save and test my_callback_method in isolation.
Now, as said in the comments, that you want to check if someone deleted the after_save, you can add a test for:
RSpec.describe Author, :type => :model do
context "Author" do
it "should define my_callback_method for after_save" do
result = Author._save_callbacks.select { |cb| cb.kind.eql?(:after) }.collect(&:filter).include?(:my_callback_method)
expect(result).to eq true
end
end
end

Your code looks correct but there is a number of outstanding issues in Mongoid related to callbacks in persistence. Ensure the callback is called in normal operation (i.e. when you save a post from a Rails console).

Related

What is 'valid?' in RSpec? Where can I look at it?

I've attempted to create a model, which needs to pass a series of validation tests in RSpec. However, I constantly get the error
expected #<Surveyor::Answer:0x0055db58e29260 #question=#<Double Surveyor::Question>, #value=5> to respond to `valid?`
My understanding (from here) was that 'valid?' checks that no errors were added to the model. I can't find any errors, however the message above persists.
This is my model
module Surveyor
class Answer
attr_accessor :question, :value
def initialize(params)
#question = params.fetch(:question)
#value = params.fetch(:value)
end
end
end
And the class Question
module Surveyor
class Question
attr_accessor :title, :type
def initialize(params)
#title = params.fetch(:title, nil)
#type = params.fetch(:type)
end
end
end
And this is the test I am attempting to pass
RSpec.describe Surveyor::Answer, '03: Answer validations' do
let(:question) { double(Surveyor::Question, type: 'rating') }
context "question validation" do
context "when the answer has a question" do
subject { described_class.new(question: question, value: 5) }
it { should be_valid }
end
end
Is my understanding of 'valid?' correct? Am I able to look at 'valid?' and perhaps see where I'm going wrong?
RSpec doesn't actually have a matcher called be_valid, instead it has some dynamic predicate matchers:
For any predicate method, RSpec gives you a corresponding matcher. Simply prefix the
method with be_ and remove the question mark. Examples:
expect(7).not_to be_zero # calls 7.zero?
expect([]).to be_empty # calls [].empty?
expect(x).to be_multiple_of(3) # calls x.multiple_of?(3)
so by calling it { should be_valid }, your subject has to respond to a valid? method. If you're testing an ActiveRecord model, those have a valid? method, but your model does not. So, if you want to test that your Answer is valid, you need to decide "what is a valid answer?" and write a method that checks for those conditions. If you want an API similar to Rails model, you might be interested in using ActiveModel::Validations

How to test class methods that rely on associations with RSpec and Sinatra?

I've written some RSpec tests that successfully create objects with :let statements. However, the test environment doesn't maintain the associations that function properly everywhere else. Below is an example of a class that would turn up a NoMethodError (undefined method `money' for nil:NilClass). Money is a column in Inventory. Any thoughts?
class Inventory < ActiveRecord::Base
belongs_to :character
def self.return_money(character)
character.inventory.money
end
end
And here's a corresponding example for a spec doc:
require 'spec_helper'
describe 'Test methods' do
let(:trader) {
Character.create(
name: "Trader",
location_id: 1)
}
let(:trader_inventory) {
Inventory.create(
character_id: trader.id,
storage_capacity: 50000,
money: 20000,
markup: 1.35)
}
it "test method" do
expect(Inventory.return_money(trader)).to eq(100)
end
end
There is no reason this shouldn't work. RSpec isn't special, it's just regular Ruby code. You can confirm this by moving all of your code into a single file, something like:
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)
class Inventory < ActiveRecord::Base
end
class Character < ActiveRecord::Base
has_one :inventory
end
describe 'test' do
it 'works' do
puts Character.first.inventory.money.inspect
end
end
Guesses as to what may be broken:
money is a composite field or something like that. Can you post your database schema?
Library files aren't being loaded correctly. Use puts $LOADED_FEATURES to verify that all the files that should be required have been.

How to verify if an embedded field changed on before_save?

I am running Ruby 2.1 and Mongoid 5.0 (no Rails).
I want to track on a before_save callback whether or not an embedded field has changed.
I can use the document.attribute_changed? or document.changed methods to check normal fields, but somehow these don't work on relations (embed_one, has_one, etc).
Is there a way of detecting these changes before saving the document?
My model is something like this
class Company
include Mongoid::Document
include Mongoid::Attributes::Dynamic
field :name, type: String
#...
embeds_one :address, class_name: 'Address', inverse_of: :address
#...
before_save :activate_flags
def activate_flags
if self.changes.include? 'address'
#self.changes never includes "address"
end
if self.address_changed?
#This throws an exception
end
end
One example of how I save my document is:
#...
company.address = AddressUtilities.parse address
company.save
#After this, the callback is triggered, but self.changes is empty...
#...
I have read the documentation and Google the hell out of it, but I can't find a solution?
I have found this gem, but it's old and doesn't work with the newer versions of Mongoid. I want to check if there is another way of doing it before considering on trying to fix/pull request the gem...
Adding these two methods to your Model and calling get_embedded_document_changes should provide you an hash with the changes to all its embedded documents:
def get_embedded_document_changes
data = {}
relations.each do |name, relation|
next unless [:embeds_one, :embeds_many].include? relation.macro.to_sym
# only if changes are present
child = send(name.to_sym)
next unless child
next if child.previous_changes.empty?
child_data = get_previous_changes_for_model(child)
data[name] = child_data
end
data
end
def get_previous_changes_for_model(model)
data = {}
model.previous_changes.each do |key, change|
data[key] = {:from => change[0], :to => change[1]}
end
data
end
[ source: https://gist.github.com/derickbailey/1049304 ]

Mongoid embedded collection response to :find

I'm sending serialized data to a class which need to access a Mongoid document which may or may not be embedded.
In case of embedded document, I'm accepting a variable number of arguments which I reduce to get the embedded document.
The code is pretty simple:
def perform(object, *arguments)
#opts = arguments.extract_options!
#object = arguments.reduce(object){|object, args| object.public_send(*args)}
# [...]
I used public_send because AFAIK I only need to call public methods.
However, when I try to access an embedded document I have some really strange result where #object is an enumerator.
After some debugging, this is what I found that for any root document object and an embedded collection items, I have:
object.items.public_send(:find)
# => #<Enumerator: ...>
object.items.send(:find) # or __send__
# => nil
The method called is not the same at all when I call public_send or send!
How is it even possible?
Is it normal? Is that a bug?
public_send seems to invoke the find method of Array (Enumerable) but send (or __send__) invokes the find method of Mongoid
Edit: simple reproductible case:
require 'mongoid'
class User
include Mongoid::Document
field :name, type: String
embeds_many :groups
end
class Group
include Mongoid::Document
field :name, type: String
embedded_in :user
end
Mongoid.load_configuration({
sessions: {
default: {
database: 'send_find',
hosts: [
'localhost:27017'
]
}
}
})
user = User.create(name: 'john')
user.groups.create(name: 'g1')
user.groups.create(name: 'g2')
puts "public_send :find"
puts user.groups.public_send(:find).inspect
# => #<Enumerator: [#<Group _id: 5530dea57735334b69010000, name: "g1">, #<Group _id: 5530dea57735334b69020000, name: "g2">]:find>
puts "send :find"
puts user.groups.send(:find).inspect
# => nil
puts "__send__ :find"
puts user.groups.__send__(:find).inspect
# => nil
Okay, after a few hours of debugging, I found that it is actually a bug in Mongoid.
The relation is not an array but a proxy around the array, which delegates most methods to the array.
As public_send was also delegated but not send and __send__, the behavior was not the same.
For more information, see my pull request and the associated commit.

nil value reaching ActiveRecord validation despite correct value passed to constructor

Validation on a model object is failing despite what I think is the correct parameter value being whitelisted in the controller and passed to the model's constructor. What am I doing wrong?
OuterModel has_one Location via locatable. The latter is created using accepts_nested_attributes_for and validates only the :country attribute:
(EDIT: I found the error, it was hidden by code that I initially left out of the code here for simplification. See my answer below)
class OuterModel < Parent
has_one :location, as: locatable
accepts_nested_attributes_for :location
end
class Parent < ActiveRecord::Base
after_create :create_location
end
class Location < ActiveRecord::Base
belongs_to :locatable, polymorphic: true
validates :country, inclusion: {in: ["US", "CA"]}
end
Controller:
class OuterModelsController < ApplicationController
def create
#outer = OuterModel.new(outer_params)
if #outer.save
byebug #debug here
redirect_to outer_path(#outer)
end
end
def outer_params
params.require(:outer).permit(:name, :type,
location_attributes: [:country, :state, :city])
end
end
Using byebug I see the #outer.save call is satisfied, but the nested location object is not persisted because of a validation error:
(byebug) #outer.persisted? #true
(byebug) #outer.location.persisted? #false
(byebug) #outer.location.valid? #false
(byebug) #outer.location.country #nil
(byebug) #outer.id #6372
(byebug) #outer.location.errors
<ActiveModel::Errors:0x007f1f33c1eae0
#base=#<Location id: nil, city: nil, country: nil, state: nil, locatable_id: 6732, locatable_type: "OuterModel", created_at: nil, updated_at: nil>,
#messages={:country=>["is not included in the list"]}>
However, the controller outer_params method appears to be sending the correct hash to OuterModel.new:
{"name"=>"A name",
"type"=>"OuterModel",
"location_attributes"=>{
"city"=>"South Deionview", "country"=>"US", "state"=>"IL"
}
}
Why then is the Location.country value nil after the call to save, and why is validation failing?
EDIT: logfile (much simplified) pastebin here. It seems like the correct values are being sent as part of the SQL
The answer lay in a parent of OuterModel that had an after_create hook to create a Location. Added the parent class to the source above.
The Location object was initially created correctly by accepts_nested_attributes_for (as per all evidence from logging) but then the after_create hook replaced it with an empty Location object that failed validation.

Resources