Rspec how to test in inject method - ruby-on-rails

I have a method that looks like this:
def self.average_top_level_comments_leaders
top_level_comment_count = CrucibleComment.group(:user_id).where(parent_comment_id: nil).order('count_all DESC').count
code_review_assigned_count = Reviewer.group(:user_id).order('count_all DESC').count
division_result = top_level_comment_count.inject({}) do |result, item|
id = item.first #id =12
count = item.last #value = 57
if (count && code_review_assigned_count[id])
result[id] = (count/ code_review_assigned_count[id]).round(2)
#result[12] = 57/12 = 3.3, => {12, 3.3}
end
result
end
end
This method returns a hash with the IDs as keys and the results of the division as the values.
I have successfully tested top_level_comment_count and code_review_assigned count, but I am having trouble figuring out how I can test the 4 other things that are in the do block:
.first, .last, .round(2), result
I am trying to test .first and this is what I have so far:
describe '#average_top_level_comments_leaders' do
subject { User.average_top_level_comments_leaders}
let(:avg_top_level_comments) { double }
let(:code_review_count) { double }
let(:item) { double( {id: 12}) }
context 'when getting the comment count succeeds ' do
before do
allow(CrucibleComment).to receive(:group).with(:user_id).and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:where).with(parent_comment_id: nil).and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:order).with('count_all DESC').and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:count).and_return(avg_top_level_comments)
allow(avg_top_level_comments).to receive(:inject).and_return(avg_top_level_comments)
allow(item).to receive(:first).and_return(item)
allow(Reviewer).to receive(:group).with(:user_id).and_return(code_review_count)
allow(code_review_count).to receive(:order).with('count_all DESC').and_return(code_review_count)
allow(code_review_count).to receive(:count).and_return(code_review_count)
allow(code_review_count).to receive(:round).with(2).and_return(code_review_count)
end
it 'and the correct parameters are called' do
expect(CrucibleComment).to receive(:group).with(:user_id)
subject
end
it 'and comment count is calling descending correctly' do
expect(avg_top_level_comments).to receive(:order).with('count_all DESC')
subject
end
it 'item gets the first result' do
expect(item).to receive(:first)
subject
end
end
end
I cannot get the last it statement to pass. I am trying to expect(item).to receive(:first), but it says this in the error:
Failure/Error: expect(item).to receive(:first)
(Double).first(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Any idea why this is not passing? The other two its are passing

The item double is never used in the test, so when it reaches:
expect(item).to receive(:first)
it fails.
If you were expecting the item double to be used within the inject block here:
division_result = top_level_comment_count.inject({}) do |result, item|
merely by virtue of it having the same name, it doesn't work that way. You'd need to define a method on the avg_top_level_comments double that returns the item double when inject is called.
But, you shouldn't do that. Throw all of this out and use real model instances for the test. It will be much easier to read and maintain.

Related

Accessing a hash inside an array

I am working on a coding problem where I have 3 lines of text and I have to calculate the words that appear the most in those lines. The answer is: ['it','really','will'] because the text is:
This is a really really really cool experiment really
Cute little experiment
Will it work maybe it will work do you think it will it will
Everything works in the code below except the highest_count_words_across_lines method. It's supposed to return ['it','really','will'] but instead returns 2 hashes inside an array:[{"a"=>1, "cool"=>1, "experiment"=>1, "is"=>1, "really"=>4, "this"=>1}, {"cute"=>1, "experiment"=>1, "little"=>1}, {"do"=>1, "it"=>4, "maybe"=>1, "think"=>1, "will"=>4, "work"=>2, "you"=>1}].
I've tried iterating through a hash with multiple select statements to no avail.
This is my full code so far:
class LineAnalyzer
attr_accessor :highest_wf_count, :highest_wf_words, :content, :line_number #Implement the following read-only attributes in the LineAnalyzer class.
def initialize(content, line)
#content = content #* initialize the content and line_number attributes
#line_number = line
#highest_wf_count = 0
calculate_word_frequency()
end
def calculate_word_frequency()
#highest_wf_words = Hash.new
words = #content.downcase.split
words.each { |w|
if #highest_wf_words.has_key?(w)
#highest_wf_words[w] += 1
else
#highest_wf_words[w] = 1
end
}
#highest_wf_words.sort_by { |word, count| count }
#highest_wf_words.each do |key, value|
if value > #highest_wf_count
#highest_wf_count = value
end
end
end
def highest_wf_count= (number)
#highest_wf_count = number
end
end
class Solution
attr_reader :analyzers, :highest_count_across_lines, :highest_count_words_across_lines # Implement the following read-only attributes in the Solution class.
def initialize()
#analyzers = []
highest_count_across_lines = nil
highest_count_words_across_lines = []
end
def analyze_file()
File.foreach('test.txt').with_index(1) do |content, line|
line_analyzer = LineAnalyzer.new(content, line)
#analyzers << line_analyzer
end
end
def calculate_line_with_highest_frequency()
#highest_count_across_lines = analyzers.map(&:highest_wf_count).max
#highest_count_words_across_lines = analyzers.select { |k,v| v = #highest_count_across_lines }
end
def print_highest_word_frequency_across_lines()
"The following words have the highest frequency per line: \n #{highest_count_words_across_lines} (appears in line #{line_num} \n"
end
end
This is the error message I get:
Failures:
1) Solution#calculate_line_with_highest_frequency calculates highest count words across lines to be will, it, really
Failure/Error: expect(words_found).to match_array ["will", "it", "really"]
expected collection contained: ["it", "really", "will"]
actual collection contained: [{"a"=>1, "cool"=>1, "experiment"=>1, "is"=>1, "really"=>4, "this"=>1}, {"cute"=>1, "experiment"=>1, "little"=>1}, {"do"=>1, "it"=>4, "maybe"=>1, "think"=>1, "will"=>4, "work"=>2, "you"=>1}]
the missing elements were: ["it", "really", "will"]
the extra elements were: [{"a"=>1, "cool"=>1, "experiment"=>1, "is"=>1, "really"=>4, "this"=>1}, {"cute"=>1, "experiment"=>1, "little"=>1}, {"do"=>1, "it"=>4, "maybe"=>1, "think"=>1, "will"=>4, "work"=>2, "you"=>1}]
# ./spec/solution_spec.rb:39:in `block (3 levels) in <top (required)>'
Finished in 0.26418 seconds (files took 0.38 seconds to load)
19 examples, 1 failure
Failed examples:
rspec ./spec/solution_spec.rb:31 # Solution#calculate_line_with_highest_frequency calculates highest count words across lines to be will, it, really
I've tried iterating through the hashes within an array but kept getting an error message. I am trying to find the keys where the values (counts) are equal to the highest count (4). So the final answer should be ["it","really","will"]. Any suggestions?
Step 1
Merge the array of hash into one single hash:
lets say, your array of hash is arrayOfHash
hash = arrayOfHash.inject(:merge)
Step 2
collect the keys which contains the maximum values in that single hash we created in step 1:
result = arrayOfHash.collect{|k, v| k if v == arrayOfHash.values.max}.compact

array.select not iterating through every element

I have a rails controller and this code only loop through the first element in the metrics array? Why is that?
# /metrics/:id
def values
#metric = metrics.select do |metric|
id = metric['href'].split('/').last
p "id == params[:id] = #{id == params[:id]}" # false on the first iteration (but never gets to the next iteration
return id == params[:id]
end
p "HERE?" # We never get here!
end
You need to remove the return statement from your method, Ruby uses implicit return (see https://jtrudell.github.io/blog/ruby_return_values/), so the result of a block is the last line that is evaluated in that block, the return statement in your code is treated as a return from the values method. Your method needs to look something like:
def values
#metric = metrics.select do |metric|
metric['href'].split('/').last == params[:id]
end
end

Rspec inconsistent results between 2 tests with same code

I have a model spec that throws an unexpected error (doesn't build the object right).
When I move the same code into a previous test, it runs smoothly.
Here's the problematic expectation:
expect(title.full_title.length).to be <= 140
When adding the line to the first test, it passes, while the second test fails:
describe "generates a title" do
let(:collection) { FactoryBot.create(:collection) }
let(:items) { FactoryBot.create_list(:item, 10, collection: collection, chosen: true) }
let(:title) { Title.create_title(collection) }
context "happy path" do
it "assigns keywords by score" do
array = []
items.each do |i|
array << [i.search.term, i.search.score]
end
array.sort! { |a, b| a[1] <=> b[1] }
split_title = title.full_title.split(', ')
remaining_chars = 140
i = 0
split_title.each do |s|
if remaining_chars - s.length >= 0
expect(s).to eq(array[i][0])
i += 1
remaining_chars -= s.length
end
expect(title.full_title.length).to be <= 140
end
end
it "does not exceed 140 characters" do
expect(title.full_title.length).to be <= 140
end
end
Here is the error message, it doesn't create the object:
1) Title generates a title happy path does not exceed 140 characters
Failure/Error: remaining_chars = 140 - keywords[0].length
NoMethodError:
undefined method `length' for nil:NilClass
TIA!
This is a long shot, but let's try if I'm lucky today.
Guess: Title.full_title implementation (which you didn't share) depends somehow on the existence of the Item's related to the collection which is used to initialize the Title object.
If that assumption is correct, different behavior comes from the lazy nature of let.
In the first it, you actually call items (e.g. items.each) so the let(:items) is evaluated, the rows in DB are created and title.full_title does not return nil.
There a few possible many ways to fix it:
Improve the implementation of full_title to always return string.
call items before in the second scenario
it "does not exceed 140 characters" do
items
expect(title.full_title.length).to be <= 140
end
use let!(:items) which is evaluated immediately (not lazy)

Model's method called in RSpec doesn't change objects in test database

I have this code in spec/models:
describe "update ranking position and player's points" do
before do
#player.save
#player2 = Player.create(firstname: "Second", lastname: "Player", nickname: "second99")
#match = Match.create(
{ loserscore: 1, winner_player_id: #player.id,
loser_player_id: #player2.id })
#player.update_rank
#player2.update_rank
Player.update_position
end
it { expect(#player.position).to eq 1 }
end
This is the method from model:
def self.update_position
#players = Player.all
#players = #players.sort_by { |player| player.rank.to_i }.reverse
#players.each_with_index do |player, index|
player.position = index + 1
player.save
end
end
When I call this method in controller everything is OK (ranking is updated).
But in RSpec I get nil
1) Player update ranking position and player's points should eq 1
Failure/Error: it { expect(#player.position).to eq 1 }
expected: 1
got: nil
I tried debug it so I wrote in RSpec:
p Player.all.count
but it shows
2
So for me everything looks OK - there are two rows in Player table.
Every others tests passed. I have the problem only with this one method (this is only one class method in this model). App works fine.
Why called method doesn't change position column in Player table?
You need to reload the instance variables after performing any updates.
it 'should set #player as winner' do
#player.reload
expect(#player.position).to eq(1)
end

Counting several elements inside an array

I just wrote a method that I'm pretty sure is terribly written. I can't figure out if there is a better way to write this in ruby. It's just a simple loop that is counting stuff.
Of course, I could use a select or something like that, but that would require looping twice on my array. Is there a way to increment several variables by looping without declaring the field before the loop? Something like a multiple select, I don't know. It's even worst when I have more counters.
Thank you!
failed_tests = 0
passed_tests = 0
tests.each do |test|
case test.status
when :failed
failed_tests += 1
when :passed
passed_tests +=1
end
end
You could do something clever like this:
tests.each_with_object(failed: 0, passed: 0) do |test, memo|
memo[test.status] += 1
end
# => { failed: 1, passed: 10 }
You can use the #reduce method:
failed, passed = tests.reduce([0, 0]) do |(failed, passed), test|
case test.status
when :failed
[failed + 1, passed]
when :passed
[failed, passed + 1]
else
[failed, passed]
end
end
Or with a Hash with default value, this will work with any statuses:
tests.reduce(Hash.new(0)) do |counter, test|
counter[test.status] += 1
counter
end
Or even enhancing this with #fivedigit's idea:
tests.each_with_object(Hash.new(0)) do |test, counter|
counter[test.status] += 1
end
Assuming Rails 4 ( using 4.0.x here). I would suggest:
tests.group(:status).count
# -> {'passed' => 2, 'failed' => 34, 'anyotherstatus' => 12}
This will group all records by any possible :status value, and count each individual ocurrence.
Edit: adding a Rails-free approach
Hash[tests.group_by(&:status).map{|k,v| [k,v.size]}]
Group by each element's value.
Map the grouping to an array of [value, counter] pairs.
Turn the array of paris into key-values within a Hash, i.e. accessible via result[1]=2 ....
hash = test.reduce(Hash.new(0)) { |hash,element| hash[element.status] += 1; hash }
this will return a hash with the count of the elements.
ex:
class Test
attr_reader :status
def initialize
#status = ['pass', 'failed'].sample
end
end
array = []
5.times { array.push Test.new }
hash = array.reduce(Hash.new(0)) { |hash,element| hash[element.status] += 1; hash }
=> {"failed"=>3, "pass"=>2}
res_array = tests.map{|test| test.status}
failed_tests = res_array.count :failed
passed_tests = res_array.count :passed

Resources