How to write rspec for a model method? - ruby-on-rails

Here's my method
def get_processing_fee_for_given_price(ticket_service_fee, price)
((processing_fee_percent / 100.0) * (ticket_service_fee.to_f + price.to_f))
end
I want to write rspec for this method.
def processing_fee_percent
3.0
end
This is my processing_fee_percent method

I would do it like this:
describe '#get_processing_fee_for_given_price' do
subject(:instance) { described_class.new }
it 'returns to expected processing_fee' do
expect(
instance.get_processing_fee_for_given_price(5, 100)
).to eq 3.15
end
end
There are accuracy problems when using floats, therefore it is not recommended to use floats for currency calculations. A very simple example of unexpected behavior with floats is this one:
0.1 + 0.2
#=> 0.30000000000000004
I suggest using BigDecimal instead.

Related

RSpec how to exception of raising error - expected ZeroDivisionError but nothing was raised

I want to check if number divided by 0 will rise ZeroDivisionError from below method
def progress_percent
(activity_reads.size / journey.cms_activity_ids.size.to_f)
rescue ZeroDivisionError
0
end
My specs are:
context 'when journey does not have activity' do
let!(:activity_read) { ['test', 'test'] }
let(:journey) { build(:journey, :without_activity) }
it 'raise the ZeroDivisionError' do
expect { call }.to raise_error(ZeroDivisionError)
end
end
When I put binding.pry to progress_percent method I'm getting what I was expected so activity_read = 2 and journey.cms_activity_ids.size.to_f = 0 but in my test I've got an error:
Failure/Error: expect { call }.to raise_error(ZeroDivisionError)
expected ZeroDivisionError but nothing was raised
EDIT:
It's a part of bigger class
def call
Progres.update(
user_id: current_user.id,
percent_progress: progress_percent,
)
end
private
def progress_percent
(activity_reads.size / journey.cms_activity_ids.size.to_f)
rescue ZeroDivisionError
0
end
why not an early return instead?:
def progress_percent
return 0 unless journey.cms_activity_ids.size > 0
activity_reads.size / journey.cms_activity_ids.size
end
also, you don't need to cast to float or integer, the "assoc_ids" AR method will return an object that responds to :size
As Spickermann says, if you're handling the error, you don't test for it.
What you should do is confirm that the method returns 0 in that scenario.
Having said that... you are converting journey.cms_activity_ids.size to a float so that won't raise an error anyway. Dividing by 0.0 will give a result of Infinity instead of raising an error.
You can test for infinity with...
expect(subject.send(:progress_percent)).to eq BigDecimal('Infinity')
If the non-zero numerator is negative, and the denominator is zero, you'll get negative infinity. Test for that with...
expect(subject.send(:progress_percent)).to eq BigDecimal('-Infinity')
Note, however, that if you have a "negative zero", it's slightly different: -1.0 / -0.0 returns Infinity, not negative Infinity.
If the numerator and denominator are both zero and at least one is a float, you will get NaN returned. Test for that with...
expect(subject.send(:progress_percent).nan?).to be true
NaN never equals anything, not even another NaN
It's not raised by call, because you catch/rescue it.
expect(call).to eq(0)
Should pass.

How to calculate the highest word frequency in Ruby

I have been working on this assignment for a Coursera Intro to Rails course. We have been tasked to write a program that calculates maximum word frequency in a text file. We have been instructed to create a method which:
Calculates the maximum number of times a single word appears in the given content and store in highest_wf_count.
Identify the words that were used the maximum number of times and store that in highest_wf_words.
When I run the rspec tests that were given to us, one test is failing. I printed my output to see what the problem is but haven't been able to fix it.
Here is my code, the rspec test, and what I get:
class LineAnalyzer
attr_accessor :highest_wf_count
attr_accessor :highest_wf_words
attr_accessor :content
attr_accessor :line_number
def initialize(content, line_number)
#content = content
#line_number = line_number
#highest_wf_count = 0
#highest_wf_words = highest_wf_words
calculate_word_frequency
end
def calculate_word_frequency()
#highest_wf_words = Hash.new(0)
#content.split.each do |word|
#highest_wf_words[word.downcase!] += 1
if #highest_wf_words.has_key?(word)
#highest_wf_words[word] += 1
else
#highest_wf_words[word] = 1
end
#highest_wf_words.sort_by{|word, count| count}
#highest_wf_count = #highest_wf_words.max_by {|word, count| count}
end
end
def highest_wf_count()
p #highest_wf_count
end
end
This is the rspec code:
require 'rspec'
describe LineAnalyzer do
subject(:lineAnalyzer) { LineAnalyzer.new("test", 1) }
it "has accessor for highest_wf_count" do
is_expected.to respond_to(:highest_wf_count)
end
it "has accessor for highest_wf_words" do
is_expected.to respond_to(:highest_wf_words)
end
it "has accessor for content" do
is_expected.to respond_to(:content)
end
it "has accessor for line_number" do
is_expected.to respond_to(:line_number)
end
it "has method calculate_word_frequency" do
is_expected.to respond_to(:calculate_word_frequency)
end
context "attributes and values" do
it "has attributes content and line_number" do
is_expected.to have_attributes(content: "test", line_number: 1)
end
it "content attribute should have value \"test\"" do
expect(lineAnalyzer.content).to eq("test")
end
it "line_number attribute should have value 1" do
expect(lineAnalyzer.line_number).to eq(1)
end
end
it "calls calculate_word_frequency when created" do
expect_any_instance_of(LineAnalyzer).to receive(:calculate_word_frequency)
LineAnalyzer.new("", 1)
end
context "#calculate_word_frequency" do
subject(:lineAnalyzer) { LineAnalyzer.new("This is a really really really cool cool you you you", 2) }
it "highest_wf_count value is 3" do
expect(lineAnalyzer.highest_wf_count).to eq(3)
end
it "highest_wf_words will include \"really\" and \"you\"" do
expect(lineAnalyzer.highest_wf_words).to include 'really', 'you'
end
it "content attribute will have value \"This is a really really really cool cool you you you\"" do
expect(lineAnalyzer.content).to eq("This is a really really really cool cool you you you")
end
it "line_number attribute will have value 2" do
expect(lineAnalyzer.line_number).to eq(2)
end
end
end
This is the rspec output:
13 examples, 1 failure
Failed examples:
rspec ./course01/module02/assignment-Calc-Max-Word-Freq/spec/line_analyzer_spec.rb:42 # LineAnalyzer#calculate_word_frequency highest_wf_count value is 3
My output:
#<LineAnalyzer:0x00007fc7f9018858 #content="This is a really really really cool cool you you you", #line_number=2, #highest_wf_count=[nil, 10], #highest_wf_words={"this"=>2, nil=>10, "is"=>1, "a"=>1, "really"=>3, "cool"=>2, "you"=>3}>
Based on the test string, the word counts aren't correct.
"nil" is being included in the hash.
The hash is not being sorted by the value (count) like it should.
I tried several things to fix these problems and nothing has worked. I went through the lecture material again, but can't find anything that would help and the discussion boards are not often monitored for questions from students.
Accoriding to Ruby documentation:
downcase!(*args) public
Downcases the contents of str, returning nil if no changes were made.
Due to this unexpected behavior of .downcase! method, if the word is already all lowercase, you're incrementing occurrences of nil in this line:
#highest_wf_words[word.downcase!] += 1
Tests are also failing because #highest_wf_words.max_by {|word, count| count} returns an array containing the count and a word, while we want to get only the count.
A simplified calculate_word_frequency method passing the tests would look like this:
def calculate_word_frequency()
#highest_wf_words = Hash.new(0)
#content.split.each do |word|
# we don't have to check if the word existed before
# because we set 0 as default value in #highest_wf_words hash
# use .downcase instead of .downcase!
#highest_wf_words[word.downcase] += 1
# extract only the count, and then get the max
#highest_wf_count = #highest_wf_words.map {|word, count| count}.max
end
end
Nil:
The nil is from downcase!
This modifies the String inplace and returns nil if nothing has changed.
If you say "this is weird", then you are right (IMHO).
# just use the non destructive variant
word.downcase
Sorting:
sort_by returns a new object (Hash, Array, ...) and does not modify the receiver of the method. You either need to re-assign or to use sort_by!
unsorted = [3, 1, 2]
sorted = unsorted.sort
p unsorted # => [3, 1, 2]
p sorted # => [1, 2, 3]
unsorted.sort!
p unsorted # => [1, 2, 3]
Faulty word count:
Once you corrected those two mistakes it should look better. Be aware, that the method does not return a single integer but a two-element array with the word and count, so it should look something like this: ["really", 6]
Simplifiying things:
If you can use ruby 2.7, then there is the handy Enumerable#tally method!
%w(foo foo bar foo baz foo).tally
=> {"foo"=>4, "bar"=>1, "baz"=>1}
Example taken from
https://medium.com/#baweaver/ruby-2-7-enumerable-tally-a706a5fb11ea

How do I pass a method with two arguments to a Minitest Spec assertion?

With Minitest Spec in Rails I'm trying to check if an ActiveSupport::TimeWithZone is in a certain range. I thought to use the between? method that takes the min and max of the range. Here's how I'm expressing that in Minitest Spec:
_(language_edit.curation_date).must_be :between?, 10.seconds.ago, Time.zone.now
but it gives me this error:
Minitest::UnexpectedError: ArgumentError: wrong number of arguments (1
for 2)
What am I doing wrong?
Looks like must_be is implemented as infect_an_assertion :assert_operator, :must_be
assert_operator
# File lib/minitest/unit.rb, line 299
def assert_operator o1, op, o2, msg = nil
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
assert o1.__send__(op, o2), msg
end
What if you use assert directly?
Example:
class DateTest < ActiveSupport::TestCase
test "using assert with between? should work" do
a = 5.seconds.ago
assert a.between?(10.seconds.ago, Time.zone.now)
end
end
Thanks to radubogdan for showing me some of the code behind the must_be method. It looks like it's designed to be used with operators like this:
_(language_edit.curation_date).must_be :>, 10.seconds.ago
and a side-effect of this is it works with boolean methods that take one or no arguments, but not with methods that take more than one argument. I think I'm supposed to do this:
_(language_edit.curation_date.between?(10.seconds.ago, Time.zone.now)).must_equal true

ruby rspec conversion from "should" to "expect" with block

I'm handling a set of rspec programs and pc seems to be forcing me to convert "should" questions to "expect".
Have been able to handle most, but having problems with the following rspec setup.
Most of the other 'should' formatting involves an answer should == something and is easily converted to expect(passed_in_value).to eql(returned_value).
In this case though, I believe it is passing in a block to add to a given number, however, i and unable to just convert it to
expect(end).to eql(6) or whatever the returned value should be.
Take a look and if you have any thoughts, please pass them on
it "adds one to the value returned by the default block" do
adder do
5
end.should == 6
end
it "adds 3 to the value returned by the default block" do
adder(3) do
5
end.should == 8
end
There're several methods to do that.
result = adder(3) do
5
end
expect(result).to eq(8)
expect do
adder(3) do
5
end
end.to eq(8)
block = -> do
5
end
expect(adder 3, &block).to eq(8)
Example from comments with respond_to:
it "has a #sum method" do
[].should respond_to(:sum) #old syntax
expect([]).to respond_to(:sum) #new syntax
end

RSpec - Date should be between two dates

How can I test a date to see if it's between two dates? I know I can do two greater-than and less-than comparisons but I want an RSpec method to check the "betweeness" of the date.
For example:
it "is between the time range" do
expect(Date.now).to be_between(Date.yesterday, Date.tomorrow)
end
I tried expect(range).to cover(subject) but no luck.
Date.today.should be_between(Date.today - 1.day, Date.today + 1.day)
Both of the syntaxes you wrote are correct RSpec:
it 'is between the time range' do
expect(Date.today).to be_between(Date.yesterday, Date.tomorrow)
end
it 'is between the time range' do
expect(Date.yesterday..Date.tomorrow).to cover Date.today
end
If you are not using Rails you won't have Date::yesterday or Date::tomorrow defined. You'll need to manually adjust it:
it 'is between the time range' do
expect(Date.today).to be_between(Date.today - 1, Date.today + 1)
end
The first version works due to RSpec's built in predicate matcher. This matcher understand methods being defined on objects, and delegates to them as well as a possible ? version. For Date, the predicate Date#between? comes from including Comparable (see link).
The second version works because RSpec defines the cover matcher.
I didn't try it myself, but according to this you should use it a bit differently:
it "is between the time range" do
(Date.yesterday..Date.tomorrow).should cover(Date.now)
end
You have to define a matcher, check https://github.com/dchelimsky/rspec/wiki/Custom-Matchers
It could be
RSpec::Matchers.define :be_between do |expected|
match do |actual|
actual[:bottom] <= expected && actual[:top] >= expected
end
end
It allows you
it "is between the time range" do
expect(Date.now).to be_between(:bottom => Date.yesterday, :top => Date.tomorrow)
end

Resources