I have a Book Model which is a ruby script that assigns prices to certain predefined Book titles mentioned in the program. Here's how the book model looks:-
class Book
attr_accessor :books
def initialize books
puts "Welcome to setting book price program"
@books = books
end
def get_prices
puts "Please enter appropriate price for each book item:-"
count = 0
@books = @books.inject({}) { |hash, book|
print "#{book.first}: "
price = STDIN.gets.chomp
while (price !~ /^[1-9]\d*$/ && price != "second hand")
puts "Price can't be 0 or a negative integer or in decimal format or alphanumeric. \nPlease input appropriate price in integer"
price = STDIN.gets.chomp #gets.chomp - throws error
end
price == "second hand" ? price = "100" : price #takes a default price
hash[book.first] = price.to_i
hash
}
end
end
books = {"The Last Samurai" => nil,
"Ruby Cookbook" => nil,
"Rails Recipes" => nil,
"Agile Development with Rails" => nil,
"Harry Potter and the Deathly Hallows" => nil}
book_details = Book.new(books)
book_details.get_prices
puts "\n*******Books Details:#{book_details.books}******\n"
I'm trying to write a test case that checks for correct input of price for each book item. If the price is entered inappropriately, it should ask the user to re enter the price correctly. The program does this fine. But I'm facing difficulties when I'm trying to mock this behavior using RSpec.
require 'spec_helper'
describe Book do
before :each do
books = {"The Last Samurai" => nil,
"Ruby Cookbook" => nil,
"Rails Recipes" => nil,
"Agile Development with Rails" => nil,
"Harry Potter and the Deathly Hallows" => nil}
@book = Book.new(books)
end
describe "#new" do
it "Should be an instance of the Book" do
@book.should be_an_instance_of Book
end
end
describe "#getprice" do
it "Should get the price in the correct format or else return appropriate error" do
puts "\n************************************************************************\n"
book_obj = @book
STDOUT.should_receive(:puts).and_return("Welcome to setting book price program")
book_obj.get_prices.should_not be_nil
book_obj.books["The Last Samurai"].stub!(:gets) {"40"} #trying to set the value for one book using Hash
book_obj.books["The Last Samurai"].should == 40 #verifying the value set for a particular key is accurate
end
end
end
You can even clone this code from Github to try this from your end. I'm using Ruby 1.9.3 and rspec 2.11.0
The error that I'm getting currently is:-
Failures:
1) Book#getprice Should get the price in the correct format or else return appropriate error
Failure/Error: book_obj.books["The Last Samurai"].stub!(:gets) {"40"} #trying to set the value for one book using Hash
TypeError:
can't define singleton
# ./spec/book_spec.rb:31:in `block (3 levels) in <top (required)>'
Finished in 7.61 seconds
2 examples, 1 failure
Failed examples:
rspec ./spec/book_spec.rb:21 # Book#getprice Should get the price in the correct format or else return appropriate error
UPDATED QUESTION
For bad user input, with the below test case I'm getting the following error. How can I correctly handle this? I've tried a couple of options, but they all seem to fail. Please see the comment for each option as part of the spec snippet.
it "Incorrect input format should return error message asking user to re input" do
puts "\n************************************************************************\n"
book_obj = @book
STDIN.stub(:gets) { "40abc" }
#book_obj.get_prices.should be_nil --> adding this line of code goes into an infinite loop with the error message below
#Price cannot be 0 or a negative integer or in decimal format or alphanumeric. \nPlease input appropriate duration in integer\n
STDOUT.should_receive(:puts).and_return("Price cannot be 0 or a negative integer or in decimal format or alphanumeric. \nPlease input appropriate duration in integer\n")
#the below two tests fails with syntax error - don't seem that easy to figure out what's going wrong
#STDOUT.should_receive("Price cannot be 0 or a negative integer or in decimal format or alphanumeric. \nPlease input appropriate duration in integer\n")
#STDOUT.should == "Price cannot be 0 or a negative integer or in decimal format or alphanumeric. \nPlease input appropriate duration in integer\n"
end
Failures:
1) Book#getprice Incorrect input format should return error message asking user to re input
Failure/Error: STDOUT.should_receive(:puts).and_return("Price cannot be 0 or a negative integer or in decimal format or alphanumeric. \nPlease input appropriate duration in integer\n")
(#<IO:0x00000001c7b298>).puts(any args)
expected: 1 time
received: 0 times
# ./spec/book_spec.rb:40:in `block (3 levels) in <top (required)>'
I would really appreciate any guidance how to get this right. Thank you.
If you run the spec with the --backtrace
flag, you'll see that the error is raised on https://github.com/rspec/rspec-mocks/blob/v2.13.0/lib/rspec/mocks/method_double.rb#L140, where rspec-mocks is trying to get the singleton class of the object being stubbed or mocked. This would work fine with instances of most classes, but there is an error in the spec.
The code sends gets
to STDIN
, but the spec is trying to stub gets
on book_obj.books["The Last Samurai"]
, which is an int
at the time, and you can't get a singleton from an int
:
$ irb
1.9.3-p392 :001 > class << 1; self; end
TypeError: can't define singleton
from (irb):1
from /Users/david/.rvm/rubies/ruby-1.9.3-p392/bin/irb:16:in `<main>'
If I understand correctly, you want to make two changes. First, move the last lines of the script to a separate file that doesn't get loaded when you run the spec e.g. bin/books (it can require book.rb and then add those lines).
Next, remove the line that stubs gets
on book_obj.books["The Last Samurai"]
and add a line that stubs gets
on STDIN
instead, before the line that invokes get_prices
(which is when the interaction w/ STDIN
happens):
STDIN.stub(:gets) { "40" }
book_obj.get_prices.should_not be_nil
That will at least get your spec passing.
In general, code that deals w/ STDIN
and STDOUT
directly is hard to spec at the object level because testing tools like rspec, minitest, etc, all use STDOUT
to present their information, so you end up with a lot of confusing noise in the shell. I'd recommend either changing the design to inject input/output streams to the book class (which can be STDIN
and STDOUT
when you run the script, but test doubles when you run rspec), or use a tool like aruba, which is designed to spec interactive shell scripts.