Rspec: Failure of spy to detect method call (my pattern is wrong) - ruby-on-rails

How do I test that a method is being called within another method?
I need to test that ListingQuery.call() calls the base_filter method
Code
module ListingsQuery
class Search
def self.call(current_user, params, query_object_override=nil)
new(current_user, params, query_object_override).call
end
def initialize(current_user, params, query_object_override=nil)
#params = params
#type = params[:market] || params[:type] || "all"
#current_user = current_user
#type = if #type.present?
#type.downcase.singularize
else
"listing"
end
#query_object = query_object_override
end
def call
relation_for(query_object)
end
private
def relation_for(query_object_instance)
if query_object_instance.class == ListingsQuery::Car
car_filters(query_object_instance).relation
else
raise ArgumentError, "ListingsQuery 'Class: #{query_object_instance.class}' does not exist"
end
end
def car_filters(query_object)
base_filters(query_object)
other_filters(query_object)
query_object
end
def query_object
#query_object ||= query_object_for(#type, #current_user) # => ListingQuery::Car
end
end
end
### Test
current_user = spy("User")
query_object_class = ListingsQuery::Car
query_object = spy(query_object_class.name)
allow_any_instance_of(RSpec::Mocks::Double).to receive(:class) { query_object_class }
allow_any_instance_of(ListingsQuery::Search).to receive(:base_filters) { query_object }
so = ListingsQuery::Search.call(current_user, {}, query_object)
expect(so).to have_received(:base_filters)
Error
1) ListingsQuery::Search#car_filters should call base_filters
Failure/Error: expect(so).to have_received(:base_filters)
(Double "ListingsQuery::Car").base_filters(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
UPDATED ALSO FAILS:
current_user = spy("User")
query_object_class = ListingsQuery::Car
search_object_class = ListingsQuery::Search
query_object = spy(query_object_class.name)
allow_any_instance_of(RSpec::Mocks::Double).to receive(:class) { query_object_class }
allow_any_instance_of(search_object_class).to receive(:base_filters) { query_object }
expect(search_object_class).to have_received(:base_filters)
search_object_class.call(current_user, {}, query_object)
Error:
Failure/Error: expect(search_object_class).to have_received(:base_filters)
#<ListingsQuery::Search (class)> expected to have received base_filters, but that object is not a spy or method has not been stubbed.

I think you're assigning the variable so to the result of ListingsQuery::Search.call(current_user, {}, query_object). This might work:
so = ListingsQuery::Search
so.call(current_user, {}, query_object)
expect(so).to have_received(:base_filters)
or the way I normally write them:
so = ListingsQuery::Search
expect(so).to receive(:base_filters)
so.call(current_user, {}, query_object)

This works:
it "should call base_filters" do
current_user = spy("User")
query_object_class = ListingsQuery::Car
search_object_class = ListingsQuery::Search
query_object = spy(query_object_class.name)
allow_any_instance_of(RSpec::Mocks::Double).to receive(:class) { query_object_class }
so = search_object_class.new(current_user, {}, query_object)
allow(so).to receive(:base_filters) { query_object }
so.call
expect(so).to have_received(:base_filters)
end
For some reason allow_any_instance_of did not work, I had to stub the specific instance. I would be curious to know why, if anyone knows.

Related

Serve stale data from cache

I'm working in a Rails app where I want to serve stale data (where changes are really small and don't matter that much but I want to upgrade the cache in the background).
One way to do this is something like this:
if Rails.cache.exist?('my_view')
Rails.cache.read('my_view')
MaybeUpdateInBackground.perform_async
end
# MaybeUpdateInBackground.rb
def perform
Rails.cache.write('my_view', render_to_string('my_view'))
end
Is there a better pattern for this in RoR?
This ended up getting the job done:
To break it down:
Dummy leverages a "stale cache" - the module can be added to any class.
When you call expensive_but_unimportant_operation_cached it will check Rails.cache - if the value is in there, it'll return it - it will trigger a "refresh" if the value in there is stale.
A refresh is a Sidekiq job that re-runs the function.
class Dummy
extend T::Sig
include WithStaleCache
sig { returns(Integer) }
def expensive_but_unimportant_operation_cached
cache_with_stale_cache(
'expensive_but_unimportant_operation',
expires_in: 1.week,
stale_in: 1.day,
)
end
sig { returns(Integer) }
def self.expensive_but_unimportant_operation
sleep 10
10
end
end
module WithStaleCache
extend T::Sig
sig do
params(
method: String,
serialized_arguments: T.nilable(T::Array[String]),
stale_in: ActiveSupport::Duration,
expires_in: ActiveSupport::Duration,
)
.returns(T.untyped)
end
def cache_with_stale_cache(
method,
serialized_arguments = nil,
stale_in:,
expires_in:
)
SolidAssert.assert(
T.unsafe(self).class.respond_to?(method),
"#{T.unsafe(self).class} must implement class method: #{method}.",
)
cache_key =
build_key(T.unsafe(self).class.to_s, method, serialized_arguments)
cached_value = Rails.cache.read(cache_key)
value = cached_value&.dig('value')
if cached_value.nil?
value =
CacheFillWorker.new.perform(
cache_key,
serialized_arguments,
{ 'expires_in' => expires_in, 'stale_in' => stale_in },
)
elsif cached_value['stale_at'].before?(Time.current)
CacheFillWorker.perform_async(
cache_key,
serialized_arguments,
{ 'expires_in' => expires_in, 'stale_in' => stale_in },
)
end
value
end
sig do
params(
klass: String,
method_name: String,
args: T.nilable(T::Array[String]),
)
.returns(String)
end
def build_key(klass, method_name, args)
"stale_cache/v1/#{args}/#{klass}/#{method_name}"
end
sig { params(cache_key: String).returns([String, String]) }
def self.class_and_method_from_cache_key(cache_key)
T.cast(cache_key.split('/').last(2), [String, String])
end
sig do
params(cache_key: String, serialized_arguments: T.nilable(T::Array[String]))
.returns(T.untyped)
end
def self.perform(cache_key, serialized_arguments)
deserialized_args =
if serialized_arguments.nil?
nil
else
serialized_arguments.map { |arg| JSON.parse(arg) }
end
class_name, method = class_and_method_from_cache_key(cache_key)
klass = class_name.constantize
if deserialized_args.nil?
klass.send(method)
else
klass.send(method, *deserialized_args)
end
end
end
# typed: true
class CacheFillWorker
extend T::Sig
include Sidekiq::Worker
# Arguments are serialized.
sig do
params(
cache_key: String,
serialized_arguments: T.nilable(T::Array[String]),
cache_options: StringKeyHash,
)
.returns(T.untyped)
end
def perform(cache_key, serialized_arguments, cache_options = {})
cached_value = Rails.cache.read(cache_key)
if cached_value.nil?
next_value = WithStaleCache.perform(cache_key, serialized_arguments)
elsif cached_value['stale_at'].before?(Time.current)
next_value = WithStaleCache.perform(cache_key, serialized_arguments)
else
# No-op since the value is not stale.
return cached_value['value']
end
stale_in = cache_options.delete('stale_in').to_i
Rails.cache.write(
cache_key,
{
'value' => next_value,
'stale_at' => stale_in.seconds.from_now,
'cached_at' => Time.zone.now,
},
cache_options,
)
next_value
end
end

getting NoMethodError Undefined method in Ruby

getting NoMethodError Undefined method service_account_id under valid_restriction? method.
Can anybody check why am I getting this error?
If you have links to resolve this, that would be helpful too. Thanks.
Error:
ERROR:
<NoMethodError: undefined method `service_account_id' for #String:0x0000560784713130>
authentication_request.rb:19:in `valid_restriction?'
Code Snippet below:
module Authentication
module AuthnGcp
class DecodedToken
PROJECT_ID_TOKEN_CLAIM_NAME = "google/compute_engine/project_id"
INSTANCE_NAME_TOKEN_CLAIM_NAME = "google/compute_engine/instance_name"
SUB_TOKEN_CLAIM_NAME = "sub"
EMAIL_TOKEN_CLAIM_NAME = "email"
AUDIENCE_TOKEN_CLAIM_NAME = "aud"
attr_reader :project_id, :instance_name, :service_account_id, :service_account_email, :audience
def initialize(decoded_token_hash:, logger:)
#decoded_token_hash = decoded_token_hash
#logger = logger
initialize_required_claims
initialize_optional_claims
end
private
def initialize_required_claims
#audience = required_token_claim_value(AUDIENCE_TOKEN_CLAIM_NAME)
#service_account_id = required_token_claim_value(SUB_TOKEN_CLAIM_NAME)
end
def initialize_optional_claims
#service_account_email = optional_token_claim_value(EMAIL_TOKEN_CLAIM_NAME)
#project_id = optional_token_claim_value(PROJECT_ID_TOKEN_CLAIM_NAME)
#instance_name = optional_token_claim_value(INSTANCE_NAME_TOKEN_CLAIM_NAME)
end
def required_token_claim_value(required_token_claim)
required_token_claim_value = token_claim_value(required_token_claim)
if required_token_claim_value.nil? || required_token_claim_value.empty?
raise Errors::Authentication::Jwt::TokenClaimNotFoundOrEmpty, required_token_claim
end
log_claim_extracted_from_token(required_token_claim, required_token_claim_value)
required_token_claim_value
end
def optional_token_claim_value(optional_token_claim)
optional_token_claim_value = token_claim_value(optional_token_claim)
if optional_token_claim_value.nil? || optional_token_claim_value.empty?
optional_token_claim_value = nil
#logger.debug(LogMessages::Authentication::Jwt::OptionalTokenClaimNotFoundOrEmpty.new(optional_token_claim))
else
log_claim_extracted_from_token(optional_token_claim, optional_token_claim_value)
end
optional_token_claim_value
end
def token_claim_value(token_claim)
token_claim_path = token_claim.split('/')
#decoded_token_hash.dig(*token_claim_path)
end
def log_claim_extracted_from_token(token_claim, token_claim_value)
#logger.debug(
LogMessages::Authentication::Jwt::ExtractedClaimFromToken.new(
token_claim,
token_claim_value
)
)
end
end
end
end
==========================================================================
module Authentication
module AuthnGcp
# This class is responsible for retrieving the correct value from the GCP token
# of the requested attribute.
class AuthenticationRequest
def initialize(decoded_token:)
#decoded_token = decoded_token
end
def valid_restriction?(restriction)
token_value =
case restriction.name
when Restrictions::PROJECT_ID
#decoded_token.project_id
when Restrictions::INSTANCE_NAME
#decoded_token.instance_name
when Restrictions::SERVICE_ACCOUNT_ID
#decoded_token.service_account_id
when Restrictions::SERVICE_ACCOUNT_EMAIL
#decoded_token.service_account_email
end
raise Errors::Authentication::AuthnGcp::JwtTokenClaimIsMissing, restriction.name if token_value.blank?
token_value == restriction.value
end
end
end
end

Ruby: adding multiple rows to a hash key

I've got a class that looks like this:
class VariableStack
def initialize(document)
#document = document
end
def to_array
#document.template.stacks.each { |stack| stack_hash stack }
end
private
def stack_hash(stack)
stack_hash = {}
stack_hash['stack_name'] = stack.name
stack_hash['boxes'] = [stack.boxes.each { |box| box_hash box }]
stack_hash
end
def box_hash(box)
box_hash = {}
content = []
box.template_variables.indexed.each { |var| content << content_array(var) }
content.delete_if(&:blank?)
box_hash.store('content', content.join("\n"))
return if box_hash['content'].empty?
box_hash
end
def content_array(var)
v = #document.template_variables.where(master_id: var.id).first
return unless v
if v.text.present?
v.format_text
elsif v.photo_id.present?
v.image.uploaded_image.url
end
end
end
The document I'm testing with has two template_variables so the desired result should be a nested hash like so:
Instead I'm getting this result:
=> [#<Stack id: 1, name: "User information">]
i.e., I'm not getting the boxes key nor it's nested content. Why isn't my method looping through the box_hash and content fields?
That's because the to_array method uses each method, which returns the object it's been called on (in this case #document.template.stacks)
Change it to the map and you may get the desired result:
def to_array
#document.template.stacks.map { |stack| stack_hash stack }
end

NoMethodError undefined method `[]=' for nil:NilClass

I created multiple classes with one test method to test wither the ruby objects get serialized correctly.
The error returned:
undefined method `[]=' for nil:NilClass
from /Users/horse/workspace/queryapi/app/models/query_model.rb:193:in `serialize'
I run the below test_query method through the rails console by initializing QueryModelTester and then invoking test_query() method on that object.
My code:
class QueryModelTester
def test_query
must = Must.new
range_criteria = RangeCriteria.new
range_criteria.gte = 20140712
range_criteria.lte = 1405134711
range = RangeBuilder.new
range.search_field = "created_time"
range.range_criteria = range_criteria
must.range = range
bool = Bool.new
bool.must = must
main_query = bool.serialize
puts main_query
end
end
Here are the model classes the above class is testing:
class RangeCriteria
#query_hash = Hash.new
attr_accessor :gte, :lte
def serialize
if(#gte.present?)
#query_hash[:gte] = #gte
end
if(#lte.present?)
#query_hash[:lte] = #lte
end
if(#gte.present? || #lte.present?)
return #query_hash
end
end
end
class RangeBuilder
#query_hash = Hash.new
attr_accessor :search_field, :range_criteria
def serialize
if(#search_field.present?)
#query_hash[#search_field] = #range_criteria.serialize
return #query_hash[:range] = #query_hash
end
end
end
class Bool
#query_hash = {}
attr_accessor :must
def serialize
if( #must.present? )
#query_hash[:must] = #must.serialize
return #query_hash[:bool] = #query_hash
end
end
end
The problem is when you initialize your #query_hash. In all your classes they are initialized in wrong scope. To fix it, you should move #query_hash = Hash.new to initialize method, like:
class RangeCriteria
def initialize
#query_hash = Hash.new
end
# ...
end
class RangeBuilder
def initialize
#query_hash = Hash.new
end
# ...
end
class Bool
def initialize
#query_hash = Hash.new
end
# ...
end
Hope that helps.
Good luck!

Why I can not call super in define_method with overloading method?

When I run code below it raise error:
implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly. (RuntimeError).
I am not sure what is the problem.
class Result
def total(*scores)
percentage_calculation(*scores)
end
private
def percentage_calculation(*scores)
puts "Calculation for #{scores.inspect}"
scores.inject {|sum, n| sum + n } * (100.0/80.0)
end
end
def mem_result(obj, method)
anon = class << obj; self; end
anon.class_eval do
mem ||= {}
define_method(method) do |*args|
if mem.has_key?(args)
mem[args]
else
mem[args] = super
end
end
end
end
r = Result.new
mem_result(r, :total)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
The error message is quite descriptive. You need to explicitly pass arguments to super when you call it inside of define_method block:
mem[args] = super(*args)

Resources