RSpec stub method with block that yield from an array - ruby-on-rails

I have a method which map a collection of object...
def ranking_per_lanes
all.map do |lane|
yield(lane) if block_given?
end
end
...and yield each item if a block has been passed:
def call
ranking_per_lanes do |lane|
[lane.percent, lane.tag]
end
end
# Output
[
[30, 'l2'],
[10, 'l1']
]
Here I'm trying to test call method by mocking ranking_per_lanes but struggle to achieve it.
I know how to mock a method in order to yield a single lane:
allow(Lane).to receive(:ranking_per_lanes).and_yield(lane)
However, how does one reproduce what the map block is doing ? I tried to do the following but it yield the entire collection:
allow(Lane).to receive(:ranking_per_lanes).and_yield([
FactoryBot.create(:lane, tag: 'l2', percent: 30),
FactoryBot.create(:lane, tag: 'l1', percent: 10)
])
I also tried to loop on the collection and mock it then:
[
FactoryBot.create(:lane, tag: 'l2', percent: 30),
FactoryBot.create(:lane, tag: 'l1', percent: 10)
].each do |lane|
allow(Lane).to receive(:ranking_per_lanes).and_yield(lane)
end
but it didn't work either :(
Thanks a lot for your help

I found a solution to my problem. It is possible to stub a method with a substitue in order to pass it an internal logic. In my case, here is what I ended up doing:
before(:each) do
allow(Lane).to receive(:ranking_per_lanes) do |&block|
[
FactoryBot.create(:lane, tag: 'l2', percent: 30),
FactoryBot.create(:lane, tag: 'l1', percent: 10)
].map(&block)
end
end
which allow me to test properly my call method:
it 'returns an array of lanes with their score' do
expect(service.send(:call)).to eq(
[
[30, 'l2'],
[10, 'l1']
]
)
end

Related

Write RSpec for include method

Need to write RSpec for specific function:
def display_type(record,house_record)
#user_value = either user will provide input through enviroment, else it will be ['dctEng']
user_value =
if ENV['USER_VALUE'].nil?
ServiceConfig[:USER_VALUE]
else
ENV['USER_VALUE'].split(',')
end
user_value.map!(&:downcase)
myArr = [
'ani',
'awe',
'emi'
]
if user_value.include?(record[:mnemonic].downcase)
return (myArr.include?(house_record[:name] || house_record[:mnemonic]) ? true : false)
else
return [true, false].sample
end
end
I tried this one:
I have added the variable record and house_record but don't know how to properly write testcase for this method
describe '#display_type' do
let(:record) do
{
name: 'test_name',
mnemonic: 'test_schema_name1'
}
end
let(:house_record) do
{
name: 'test_name',
mnemonic: 'test_mnemonic',
row: true,
col: true
}
end
let(:user_value) do [
'test_name1',
'test_name2'
] end
let(:myArr) do [
'test1',
'test2',
'test3',
'test4'
] end
#updated_code:
it 'should include record[:mnemonic] in myArr array' do
result = display_type(record,house_record)
expect([true]).to include(result)
end
it 'should not include record[:mnemonic] in myArr array' do
result = display_type(record,house_record2)
expect([true,false]).to include(result)
end
end
But don't know how to complete it, getting continuous error:
It is not coming under IF block return statement, I tried to check it using binding.pry, it is giving true for
if user_value.include?(record[:mnemonic].downcase)
return (myArr.include?(house_record[:name] || house_record[:mnemonic]) ? true : false)
But while running rspec it is going to else block and do sampling based on true and false
Kindly, give a look, I updated one case, how to improve it, because it is not going in if condition

Combine first elements of multidimensional array

Let's say I have an array of product IDs and Quantities, like this:
records = [[1, 10], [1, 30], [4, 10], [4, 100], [5, 45]]
What's the easiest/most efficient way in Ruby to achieve a hash of the combined products and quantities, like this?
products_needed = [{id: 1, count:40}, {id: 4, count: 110}, {id:5, count:45}]
Try this:
records.group_by(&:first).map do |id, records_for_id|
{
id: id,
count: records_for_id.sum(&:last)
}
end
If you're in Ruby 2.4+, you can use group_by followed by transform_values:
records.group_by(&:first) # => {1=>[[1, 10], [1, 30]], 4=>[[4, 10], [4, 100]], 5=>[[5, 45]]}
records.group_by(&:first).transform_values do |values|
values.sum(&:last)
end # => {1=>40, 4=>110, 5=>45}
records
.each_with_object(Hash.new(0)){|(k, v), h| h.merge!(k => v){|_, v1, v2| v1 + v2}}
# => {1=>40, 4=>110, 5=>45}
records
.each_with_object(Hash.new(0)){|(k, v), h| h.merge!(k => v){|_, v1, v2| v1 + v2}}
.map{|k, v| {id: k, count: v}}
# => [{:id=>1, :count=>40}, {:id=>4, :count=>110}, {:id=>5, :count=>45}]
You don't have an array of product IDs and Quantities. You have an array of arrays of integers. The easiest way to deal with this array of arrays of integers is to not have an array of arrays of integers but an Order:
class Product
def to_s; 'Some Product' end
alias_method :inspect, :to_s
end
class LineItem
attr_reader :product, :count
def initialize(product, count)
self.product, self.count = product, count
end
def to_s; "#{count} x #{product}" end
alias_method :inspect, :to_s
private
attr_writer :product, :count
end
class Order
include Enumerable
def initialize(*line_items)
self.line_items = line_items
end
def each(&blk) line_items.each(&blk) end
def items
group_by(&:product).map {|product, line_items| LineItem.new(product, line_items.sum(&:count)) }
end
def to_s; line_items.map(&:to_s).join(', ') end
alias_method :inspect, :to_s
private
attr_accessor :line_items
end
Now, assuming that you receive your data in the form of an Order, instead of an array of arrays of integers, like this:
product1 = Product.new
product4 = Product.new
product5 = Product.new
order = Order.new(
LineItem.new(product1, 10),
LineItem.new(product1, 30),
LineItem.new(product4, 10),
LineItem.new(product4, 100),
LineItem.new(product5, 45)
)
All you need to do is:
order.items
#=> [40 x Some Product, 110 x Some Product, 45 x Some Product]
The bottom line is: Ruby is an object-oriented language, not an array-of-arrays-of-integers-oriented language, if you use rich objects instead of arrays of arrays of integers, your problem will become much simpler.
Note: I used order processing as an example. If your problem domain is warehouse management or something else, there will be a similar solution.

Rails - How to add values in seperate objects together

You have an array of objects which contain a key-value reference.
[{booking_ref: 'w578383', foo: 'bar', price1: 500, price2: 30],
{booking_ref: 'w578383', foo: 'bar', price1: 600, price2: 40},
{booking_ref: 'r123523', foo: 'bar', price1: 699, price2: 4}]
I want to:
group objects by key-value reference (booking_ref)
go through those groupings and to each object add the financial values only together (price1 and price2 to the other booking's price1 and price2)
collapse out of groupings back to an array of objects which contain a key-value reference. That would now be:
[{booking_ref: 'w578383', foo: 'bar', price1: 1100, price2: 70},
{booking_ref: 'r123523', foo: 'bar', price1: 699, price2: 4}]
I am thinking:
objects.group_by(&:booking_ref).each {|group|
group.merge {|key, value1, value2| value1 + value2 if key == price1 || price2}
}
Does that work and if so how do I then return them back out of the group_by state?
Whenever you can use Enumerable#group_by you can use some form of Hash#merge or Hash#update (aka merge!), and vice-versa. Others have used group_by, so here's a hash-merge answer.
Letting the variable bookings equal your array of hashes, you can write the following.
keys_to_aggregate = [:price1, :price2]
bookings.each_with_object({}) { |g,h| h.update(g[:booking_ref]=>g) { |_,o,n|
keys_to_aggregate.reduce(o) { |f,k| f.merge(k=>o[k] + n[k]) } } }.values
#=> [{:booking_ref=>"w578383", :foo=>"bar", :price1=>1100, :price2=>70},
# {:booking_ref=>"r123523", :foo=>"bar", :price1=>699, :price2=>4}]
Note that before Hash#values at the end of the expression is evaluated we have the following.
bookings.each_with_object({}) { |g,h| h.update(g[:booking_ref]=>g) { |_,o,n|
keys_to_aggregate.reduce(o) { |f,k| f.merge(k=>o[k] + n[k]) } } }
#=> {"w578383"=>{:booking_ref=>"w578383", :foo=>"bar", :price1=>1100, :price2=>70},
# "r123523"=>{:booking_ref=>"r123523", :foo=>"bar", :price1=>699, :price2=>4}}
This uses the form of Hash#update that employs a block to determine the values of keys that are present in both hashes being merged. See the doc for details, particularly the definitions of the value-determining block's three variables (k, o and n). (I've substituted _ for k [the key] to signify that it is not used in the block calculation.)
With hash objects, you could calculate the sums and merge them back to the first hash in each group :
bookings = [
{booking_ref: 'w578383', foo: 'bar', price1: 500, price2: 30},
{booking_ref: 'w578383', foo: 'bar', price1: 600, price2: 40},
{booking_ref: 'r123523', foo: 'bar', price1: 699, price2: 4}
]
grouped_bookings = bookings.group_by{ |h| h[:booking_ref] }.map do |ref, hs|
sums = hs.each_with_object(Hash.new(0)) do |h, sum|
%i(price1 price2).each do |price|
sum[price] += h[price].to_i
end
end
hs.first.merge(sums)
end
p grouped_bookings
# [{:booking_ref=>"w578383", :foo=>"bar", :price1=>1100, :price2=>70},
# {:booking_ref=>"r123523", :foo=>"bar", :price1=>699, :price2=>4}]
Taking an object-oriented approach here, you can get this looking quite neat and elegant. The key is to define the + method on the object. Full example below:
class Booking
attr_accessor :booking_ref, :foo, :price1, :price2
def initialize(params={})
params.each { |key, value| send "#{key}=", value }
end
# add up prices, and return new Booking object
def +(other)
new_obj = self.dup
new_obj.price1 += other.price1
new_obj.price2 += other.price2
new_obj
end
end
# set up example bookings
bookings = [
Booking.new(booking_ref: 'w578383', foo: 'bar', price1: 500, price2: 30),
Booking.new(booking_ref: 'w578383', foo: 'bar', price1: 600, price2: 40),
Booking.new(booking_ref: 'r123523', foo: 'bar', price1: 699, price2: 4)
]
# grouping becomes very simple - potentially a one-liner
bookings.group_by(&:booking_ref).map { |_, values| values.reduce(&:+) }
# => [
#<Booking:... #booking_ref="w578383", #foo="bar", #price1=1100, #price2=70>,
#<Booking:... #booking_ref="r123523", #foo="bar", #price1=699, #price2=4>
]
First, group_by is fine, then use a map to iterate the hash and then reduce the value from a list to a sum using inject
objects.group_by(&:booking_ref)
.map{|ref, list| {booking_ref: ref,
price1: => list.inject(0){|sum,h| sum + h.price1},
price2: => list.inject(0){|sum,h| sum + h.price2}
}
}

Process nested hash to convert all values to strings

I have the following code which takes a hash and turns all the values in to strings.
def stringify_values obj
#values ||= obj.clone
obj.each do |k, v|
if v.is_a?(Hash)
#values[k] = stringify_values(v)
else
#values[k] = v.to_s
end
end
return #values
end
So given the following hash:
{
post: {
id: 123,
text: 'foobar',
}
}
I get following YAML output
--- &1
:post: *1
:id: '123'
:text: 'foobar'
When I want this output
---
:post:
:id: '123'
:text: 'foobar'
It looks like the object has been flattened and then been given a reference to itself, which causes Stack level errors in my specs.
How do I get the desired output?
A simpler implementation of stringify_values can be - assuming that it is always a Hash. This function makes use of Hash#deep_merge method added by Active Support Core Extensions - we merge the hash with itself, so that in the block we get to inspect each value and call to_s on it.
def stringify_values obj
obj.deep_merge(obj) {|_,_,v| v.to_s}
end
Complete working sample:
require "yaml"
require "active_support/core_ext/hash"
def stringify_values obj
obj.deep_merge(obj) {|_,_,v| v.to_s}
end
class Foo
def to_s
"I am Foo"
end
end
h = {
post: {
id: 123,
arr: [1,2,3],
text: 'foobar',
obj: { me: Foo.new}
}
}
puts YAML.dump (stringify_values h)
#=>
---
:post:
:id: '123'
:arr: "[1, 2, 3]"
:text: foobar
:obj:
:me: I am Foo
Not sure what is the expectation when value is an array, as Array#to_s will give you array as a string as well, whether that is desirable or not, you can decide and tweak the solution a bit.
There are two issues. First: the #values after the first call would always contain an object which you cloned in the first call, so in the end you will always receive a cloned #values object, no matter what you do with the obj variable(it's because of ||= operator in your call). Second: if you remove it and will do #values = obj.clone - it would still return incorrect result(deepest hash), because you are overriding existing variable call after call.
require 'yaml'
def stringify_values(obj)
temp = {}
obj.each do |k, v|
if v.is_a?(Hash)
temp[k] = stringify_values(v)
else
temp[k] = v.to_s
end
end
temp
end
hash = {
post: {
id: 123,
text: 'foobar',
}
}
puts stringify_values(hash).to_yaml
#=>
---
:post:
:id: '123'
:text: foobar
If you want a simple solution without need of ActiveSupport, you can do this in one line using each_with_object:
obj.each_with_object({}) { |(k,v),m| m[k] = v.to_s }
If you want to modify obj in place pass obj as the argument to each_with_object; the above version returns a new object.
If you are as aware of converting values to strings, I would go with monkeypatching Hash class:
class Hash
def stringify_values
map { |k, v| [k, Hash === v ? v.stringify_values : v.to_s] }.to_h
end
end
Now you will be able to:
require 'yaml'
{
post: {
id: 123,
text: 'foobar'
},
arr: [1, 2, 3]
}.stringify_values.to_yaml
#⇒ ---
# :post:
# :id: '123'
# :text: foobar
# :arr: "[1, 2, 3]"
In fact, I wonder whether you really want to scramble Arrays?

getting attributes from a hash/difference between '.first' and '[i]'

I have the following class methods for goal
def evals
self.evaluations.order("eval_number").group_by(&:student_id)
end
def evals_for(student, i)
#evals = []
self.evals.values.each do |eval|
#evals << eval.keep_if { |e| e.student_id == student.id }
end
#evals = #evals.reject { |array| array.empty? }.first
#evals[i]
end
in the view, i'm calling the second method like this:
<% #student.student_group.eval_count.times do |i| %>
<td><%= goal.evals_for(#student, i) %></td>
<% end %>
which returns
#<Evaluation:x>, #<Evaluation:y>, #<Evaluation:z>
if i change the last line of the class method to call #evals[i].inspect, i can see the data inside each hash, like so:
#<Evaluation id: 1949, score: 3, created_at: "2013-08-28 09:44:32", updated_at: "2013-08-28 09:44:32", student_id: 32, goal_id: 63, eval_number: 29>
I want to get the score, but when I call #evals[i].score on the last line in the class method, i get an error - undefined method 'score' for nil:NilClass
I know I can't call class methods on hashes, but is there a way to pull out just that data? As a sub-question, I'm a bit confused about the difference between .first and [i] as calling #evals.first.score returns "3" - only I can't use first as I need to be able to access each instance of evaluation in turn.
This is not a simple Hash:
#<Evaluation id: 1949, score: 3, created_at: "2013-08-28 09:44:32", updated_at: "2013-08-28 09:44:32", student_id: 32, goal_id: 63, eval_number: 29>
It's an instance of Evaluation class so you should be able to call score on it. I think your issue is due to the fact that you are trying to call score on nil
You'll probably want to make sure that you have an instance object before calling the method:
#evals[i].score if #evals[i]

Resources