RSpec mock method inside of select loop - ruby

I want to test simple class which iterate through array of hashes and return only those with status Pending which were updated more than 2 days ago.
class FetchPending
PROJECT_KEY = 'TPFJT'
TWO_DAYS = Time.now - 2 * 24 * 60 * 60
def call
project.select do |issue|
issue.fields.dig('status', 'name') == 'Pending' &&
DateTime.parse(issue.fields.dig('updated')) < TWO_DAYS
end
end
private
def project
#project ||= Jira::ProjectConnection.new(PROJECT_KEY).call
end
end
How to test fields method which is a method of Jira-Ruby gem. I think it comes from here (Field class in resource of gem) because nowhere else have I found fields method.
Here are my thoughts after debugging:
project.class - Array
issue.class - JIRA::Resource::Issue
my natural thinking was:
before do
# (...) some other mocks
allow(JIRA::Resource::Issue).to receive(:fields)
end
But I'm getting an error:
Failure/Error: allow(JIRA::Resource::Issue).to receive(:fields)
JIRA::Resource::Issue does not implement: fields
I have been struggling with this problem for DAYS, I'm pretty desperate here. How to mock this method?
Here is my rest of my specs:
RSpec.describe FetchPending do
subject { described_class.new }
let(:project_hash) do
[
{
'key': 'TP-47',
'fields': {
'status': {
'name': 'Pending'
},
'assignee': {
'name': 'michael.kelso',
'emailAddress': 'michael.kelso#example.com'
},
'updated': '2020-02-19T13:20:50.539+0100'
}
}
]
end
let(:project) { instance_double(Jira::ProjectConnection) }
before do
allow(Jira::ProjectConnection).to receive(:new).with(described_class::PROJECT_KEY).and_return(project)
allow(project).to receive(:call).and_return(project_hash)
allow(JIRA::Resource::Issue).to receive(:fields)
end
it 'return project hash' do
expect(subject.call).include(key[:'TP-47'])
end

and_return is generally used for returning a value (such as a string or an integer) or sequence of values, but for objects you sometimes need use a block. Additionally, if call is a valid method on a Jira::ProjectConnection object that returns the value of project_hash, you can directly mock its behavior when declaring your instance double (this functionality is unclear from the Relish docs bc they are kinda terrible). Something like this will probably work:
let(:project) { instance_double(Jira::ProjectConnection, call: project_hash) }
before do
# Ensure new proj conns always return mocked 'project' obj
allow(Jira::ProjectConnection).to receive(:new).with(
described_class::PROJECT_KEY
) { project }
end
If it still doesn't work, try temporarily replacing described_class::PROJECT_KEY with anything to debug; this can help you confirm if you specified the wrong arg(s) being sent to new.
With regard to the error message, it looks like JIRA::Resource::Issue doesn't have a fields attribute/method, though fields appears to be nested in attrs? The JIRA::Resource::Project#issues method also translates the issues in the JSON into Issue objects, so if you're using that method you will need to change the contents of project_hash.

Related

In RSpec, how to prepend a before hook in a subcontext that accesses the subject?

Consider the following:
describe MyModel do
context 'updates fields' do
subject { create(:my_model) }
before do
subject.save
subject.reload
end
context 'when changing foo.bar' do
before { subject.foo.bar = 3 }
it { is_expected.to be_multiple_bar }
end
context 'when changing baz.quux' do
before { subject.baz.quux = 3 }
it { is_expected.to be_multiple_quux }
end
end
end
Now, as you may expect, I want the before hook on line 4 to be invoked after the ones on lines 10 and 15.
I've tried 2 things:
I have tried using prepend_before, but that only works when they're defined in the same context, it doesn't allow you to prepend a hook before one that's defined in the supercontext
I have tried using before(:context) on line 10 and 15, and while this should put them in the right order, RSpec doesn't allow me to mutate the subject at that point yet. (And for good reason, I'm not trying to create a shared state here.)
I really don't want to resort to let(:append_before) { proc { #magic here } }, because it's ugly and hacky as hell. Besides, I think what I want is totally reasonable. Right now I copied the two lines over to all subcontexts, which I am not too happy with.
What is a better way to do this?
I am on RSpec 3.7, FactoryGirl 4.8.0 and Ruby 2.3.1
I don't know what your factory looks like, but instead to creating and persisting my_model, modifying, saving and reloading it, you should create it only once. This will also speed up your specs.
You could write something like this:
describe MyModel do
context 'updates fields' do
subject { create(:my_model, foo: {bar: bar}, baz: {quux: quux}) }
context 'when changing foo.bar' do
let(:bar) { 3 }
it { is_expected.to be_multiple_bar }
end
context 'when changing baz.quux' do
let(:quux) { 3 }
it { is_expected.to be_multiple_quux }
end
end
end
Lazy evaluation of both let and subject makes sure you all your parameters are set correctly depending on the context. In case you need/want to extend your factory to support that, check out http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md
I hope that makes sense.

First attempt in Ruby Rspec

I am new to Ruby and Rspec, and so I happened to found this bit of code:
Here is my Specification:
RSpec.describe Surveyor::Answer, '03: Answer validations' do
context "for a free text question" do
let(:question) { double(Surveyor::Question, type: 'free_text') }
# NOTE: The rating validations should not apply for 'free_text' questions.
subject { described_class.new(question: question, value: 'anything') }
it { should be_valid }
end
Here is my Class:
module Surveyor
class Answer
def initialize(question_answer)
#question = question_answer[:question]
#answer = question_answer[:value]
end
def question_type
# I want to check what is the type of question here.
# 'free_text' or 'rating'
# if free_text
# print question type
# else
# do something
end
end
My question is how can I print(puts) the type of question (free_text/rating) in Answer class?
When I tried using print question_answer[:question]it only gave me #<Double Surveyor::Question>
So I could not use question_answer[:question][:type]
You can access the type in the constructor simply: question_answer[:question].type, or later in object level methods: #question.type.
You can't access it like question_answer[:question][:type] because the double method in the test creates a classic like object rather than a hash.
A tip: when a method accepts parameters as a single hash you can simply name that as options or params but if you have only 3-4 params, you can use separate variables for params instead of a hash

Assignment method created using define_singleton_method returns the wrong value

Background
The Entity class is a base class that gets inherited by several subclasses that holds entities received over a REST API. The entity classes are immutable and should return a new instance of themselves whenever a change is attempted.
The Entity class has an .update() method that takes a hash of values to update, if the changes aren't really changes it returns itself and if there are real changes it returns a new instance of itself with the changes effected before instantiation.
To be user friendly Entity also allows for direct assignment to properties (so that if a subclass of Entity has a name attribute you can do instance.name = 'New Name') that also returns a new instance of the class. This is implemented in terms of update using dynamic methods that are created when the class is instantiated.
And they are the problem.
Problem
The code in the Entity class looks, in part, like this (for a complete code listing and tests check out the Github repo: https://github.com/my-codeworks/fortnox-api.git):
require "virtus"
require "ice_nine"
class Entity
extend Forwardable
include Virtus.model
def initialize( hash = {} )
super
create_attribute_setter_methods
IceNine.deep_freeze( self )
end
def update( hash )
attributes = self.to_hash.merge( hash )
return self if attributes == self.to_hash
self.class.new( attributes )
end
private
def create_attribute_setter_methods
attribute_set.each do |attribute|
name = attribute.options[ :name ]
create_attribute_setter_method( name )
end
end
def create_attribute_setter_method( name )
self.define_singleton_method "#{name}=" do | value |
self.update( name => value )
end
end
end
Doing this:
instance.update( name: 'New Name' )
and this:
instance.name = 'New Name'
Should be the same, literally since one is implemented in terms of the other.
While .update() works perfectly the .attr=() methods return the value you assign.
So in the above example .update() returns a new instance of the Entity subclass but .attr=() returns 'New Name' ...
I have tries capturing the output inside the .attr=() method and log it before returning so that I have this:
self.define_singleton_method "#{name}=" do | value |
p "Called as :#{name}=, redirecting to update( #{name}: #{value} )"
r = self.update( name => value )
p "Got #{r} back from update"
return r
end
And the log lines say:
"Called as :name=, redirecting to update( name: 'New Name' )"
"Got #<TestEntity:0x007ffedbd0ad18> back from update"
But all I get is the string 'New Name'...
My forehead is bloody and no posts I find show anything close to this. I bet I'm doing something wrong but I can't find it.
Getting dirty
The Github repo has tests in rspec that you can run, the failing ones are focused right now and some extra logging is in the Entity class to capture the different internal steps.
Comments, links and/or pull requests are welcome.
Turns out that the = methods always return the value being assigned.
o = Struct.new(:key).new(1)
o.define_singleton_method("something") { #something }
o.define_singleton_method("something=") do |v|
#something = v
return 6
end
As you can see, I've 'fixed' the return value to 6 each time something= is called. Let's see if it works:
o.something = 1 #=> outputs 1, not 6
o.something #=> outputs 1, so the method did indeed run
Conclusion? My guess is that an = method will return the value that you are assigning through it. And IMO it's better this way; one reason would be to ensure proper functioning of assignment chains:
new_val = o.something = some_val

how to use rspec to mock a class method inside a module

Im trying to test a create method that has a call to an external API but I'm having trouble mocking the external API request. Heres my setup and what I've tried so far:
class Update
def self.create(properties)
update = Update.new(properties)
begin
my_file = StoreClient::File.get(properties["id"])
update.filename = my_file.filename
rescue
update.filename = ""
end
update.save
end
end
context "Store request fails" do
it "sets a blank filename" do
store_double = double("StoreClient::File")
store_double.should_receive(:get).with(an_instance_of(Hash)).and_throw(:sad)
update = Update.create({ "id" => "222" })
update.filename.should eq ""
end
end
at the moment Im getting this failure
Failure/Error: store_double.should_receive(:get).with(an_instance_of(Hash)).and_throw(:sad)
(Double "StoreClient::File").get(#<RSpec::Mocks::ArgumentMatchers::InstanceOf:0x000001037a9208 #klass=Hash>)
expected: 1 time
received: 0 times
why is my double not working and how is best to mock the call to StoreClient::File.get, so that I can test the create method when it succeeds or fails?
The problem is that double("StoreClient::File") creates a double called "StoreClient::File", it does not actually substitute itself for the real StoreClient::File object.
In your case I don't think you actually need a double. You can stub the get method on the StoreClient::File object directly as follows:
context "Store request fails" do
it "sets a blank filename" do
StoreClient::File.should_receive(:get).with(an_instance_of(Hash)).and_throw(:sad)
update = Update.create({ "id" => "222" })
update.filename.should eq ""
end
end

List dynamic attributes in a Mongoid Model

I have gone over the documentation, and I can't find a specific way to go about this. I have already added some dynamic attributes to a model, and I would like to be able to iterate over all of them.
So, for a concrete example:
class Order
include Mongoid::Document
field :status, type: String, default: "pending"
end
And then I do the following:
Order.new(status: "processed", internal_id: "1111")
And later I want to come back and be able to get a list/array of all the dynamic attributes (in this case, "internal_id" is it).
I'm still digging, but I'd love to hear if anyone else has solved this already.
Just include something like this in your model:
module DynamicAttributeSupport
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
def dynamic_attributes
attributes.keys - _protected_attributes[:default].to_a - fields.keys
end
def static_attributes
fields.keys - dynamic_attributes
end
end
end
and here is a spec to go with it:
require 'spec_helper'
describe "dynamic attributes" do
class DynamicAttributeModel
include Mongoid::Document
include DynamicAttributeSupport
field :defined_field, type: String
end
it "provides dynamic_attribute helper" do
d = DynamicAttributeModel.new(age: 45, defined_field: 'George')
d.dynamic_attributes.should == ['age']
end
it "has static attributes" do
d = DynamicAttributeModel.new(foo: 'bar')
d.static_attributes.should include('defined_field')
d.static_attributes.should_not include('foo')
end
it "allows creation with dynamic attributes" do
d = DynamicAttributeModel.create(age: 99, blood_type: 'A')
d = DynamicAttributeModel.find(d.id)
d.age.should == 99
d.blood_type.should == 'A'
d.dynamic_attributes.should == ['age', 'blood_type']
end
end
this will give you only the dynamic field names for a given record x:
dynamic_attribute_names = x.attributes.keys - x.fields.keys
if you use additional Mongoid features, you need to subtract the fields associated with those features:
e.g. for Mongoid::Versioning :
dynamic_attribute_names = (x.attributes.keys - x.fields.keys) - ['versions']
To get the key/value pairs for only the dynamic attributes:
make sure to clone the result of attributes(), otherwise you modify x !!
attr_hash = x.attributes.clone #### make sure to clone this, otherwise you modify x !!
dyn_attr_hash = attr_hash.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
or in one line:
x.attributes.clone.delete_if{|k,v| ! dynamic_attribute_names.include?(k)}
So, what I ended up doing is this. I'm not sure if it's the best way to go about it, but it seems to give me the results I'm looking for.
class Order
def dynamic_attributes
self.attributes.delete_if { |attribute|
self.fields.keys.member? attribute
}
end
end
Attributes appears to be a list of the actual attributes on the object, while fields appears to be a hash of the fields that were predefined. Couldn't exactly find that in the documentation, but I'm going with it for now unless someone else knows of a better way!
try .methods or .instance_variables
Not sure if I liked the clone approach, so I wrote one too. From this you could easily build a hash of the content too. This merely outputs it all the dynamic fields (flat structure)
(d.attributes.keys - d.fields.keys).each {|a| puts "#{a} = #{d[a]}"};
I wasn't able to get any of the above solutions to work (as I didn't want to have to add slabs and slabs of code to each model, and, for some reason, the attributes method does not exist on a model instance, for me. :/), so I decided to write my own helper to do this for me. Please note that this method includes both dynamic and predefined fields.
helpers/mongoid_attribute_helper.rb:
module MongoidAttributeHelper
def self.included(base)
base.extend(AttributeMethods)
end
module AttributeMethods
def get_all_attributes
map = %Q{
function() {
for(var key in this)
{
emit(key, null);
}
}
}
reduce = %Q{
function(key, value) {
return null;
}
}
hashedResults = self.map_reduce(map, reduce).out(inline: true) # Returns an array of Hashes (i.e. {"_id"=>"EmailAddress", "value"=>nil} )
# Build an array of just the "_id"s.
results = Array.new
hashedResults.each do |value|
results << value["_id"]
end
return results
end
end
end
models/user.rb:
class User
include Mongoid::Document
include MongoidAttributeHelper
...
end
Once I've added the aforementioned include (include MongoidAttributeHelper) to each model which I would like to use this method with, I can get a list of all fields using User.get_all_attributes.
Granted, this may not be the most efficient or elegant of methods, but it definitely works. :)

Resources