Testing recursive IO prompt in rspec - ruby

I am writing a connect-four game for more OO and rspec practice. As part of my program, I would like to prompt the user to choose a column that they would like to put their game piece in. Here's that method:
def get_col_choice(input, output)
output.print "Player #{#player}, choose a column from 0-6: "
input_string = input.gets.chomp
begin
col_choice = Integer(input_string)
raise("The column you chose is out of bounds.") if out_of_bounds?(col_choice)
raise("The column you chose is fully occupied.") if unavailable?(col_choice)
return col_choice
rescue TypeError, ArgumentError
output.puts "Your choice of column is invalid. Try again."
rescue RuntimeError => err
puts err.message
end
get_col_choice(input, output)
end
In IRB, everything works as I planned. My hangup is in rspec where I am faced with a NoMethodError, which I think is coming from my recursive call to get_col_choice.
Can anyone help me understand what I can do to either improve get_col_choice or write the correct tests in Rspec? This is what my console output from my rspec file looks like:
Failures:
1) ConnectFourGame#get_col_choice notifies users which player gets to choose a column
Failure/Error: $connect_four.get_col_choice(input, output)
NoMethodError:
undefined method `chomp' for nil:NilClass
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:31:in `get_col_choice'
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:42:in `get_col_choice'
# ./connect_four_game_spec.rb:52:in `block (3 levels) in <top (required)>'
2) ConnectFourGame#get_col_choice returns the player's column choice
Failure/Error: expect($connect_four.get_col_choice(input, output)).to eq(0)
NoMethodError:
undefined method `chomp' for nil:NilClass
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:31:in `get_col_choice'
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:42:in `get_col_choice'
# ./connect_four_game_spec.rb:57:in `block (3 levels) in <top (required)>'
3) ConnectFourGame#get_col_choice notifies the user if the column they chose is already full of pieces
Failure/Error: expect(output.string).to include("The column you chose is fully occupied.")
expected "" to include "The column you chose is fully occupied."
# ./connect_four_game_spec.rb:62:in `block (3 levels) in <top (required)>'
4) ConnectFourGame#get_col_choice notifies the user their input is invalid when a non-numeric string is entered
Failure/Error: $connect_four.get_col_choice(input, output)
NoMethodError:
undefined method `chomp' for nil:NilClass
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:31:in `get_col_choice'
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:42:in `get_col_choice'
# ./connect_four_game_spec.rb:67:in `block (3 levels) in <top (required)>'
5) ConnectFourGame#get_col_choice column choice is out of bounds notifies the user their column choice is out of bounds when col is greater than 6
Failure/Error: $connect_four.get_col_choice(input, output)
NoMethodError:
undefined method `chomp' for nil:NilClass
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:31:in `get_col_choice'
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:42:in `get_col_choice'
# ./connect_four_game_spec.rb:74:in `block (4 levels) in <top (required)>'
6) ConnectFourGame#get_col_choice column choice is out of bounds notifies the user their column choice is out of bounds when col is less than 0
Failure/Error: $connect_four.get_col_choice(input, output)
NoMethodError:
undefined method `chomp' for nil:NilClass
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:31:in `get_col_choice'
# /home/learnsometing/Desktop/the_Odin_Project/ruby/connect-four/lib/connect_four_game.rb:42:in `get_col_choice'
# ./connect_four_game_spec.rb:80:in `block (4 levels) in <top (required)>'
Finished in 0.02399 seconds (files took 0.1108 seconds to load)
12 examples, 6 failures
Failed examples:
rspec ./connect_four_game_spec.rb:51 # ConnectFourGame#get_col_choice notifies users which player gets to choose a column
rspec ./connect_four_game_spec.rb:56 # ConnectFourGame#get_col_choice returns the player's column choice
rspec ./connect_four_game_spec.rb:60 # ConnectFourGame#get_col_choice notifies the user if the column they chose is already full of pieces
rspec ./connect_four_game_spec.rb:66 # ConnectFourGame#get_col_choice notifies the user their input is invalid when a non-numeric string is entered
rspec ./connect_four_game_spec.rb:73 # ConnectFourGame#get_col_choice column choice is out of bounds notifies the user their column choice is out of bounds when col is greater than 6
rspec ./connect_four_game_spec.rb:79 # ConnectFourGame#get_col_choice column choice is out of bounds notifies the user their column choice is out of bounds when col is less than 0
Here are the tests I wrote for get_col_choice:
describe '#get_col_choice' do
let(:output) { StringIO.new }
let(:input) { StringIO.new("0\n") }
it 'notifies users which player gets to choose a column' do
$connect_four.get_col_choice(input, output)
expect(output.string).to include("Player 2, choose a column from 0-6: ")
end
it "returns the player's column choice" do
expect($connect_four.get_col_choice(input, output)).to eq(0)
end
it 'notifies the user if the column they chose is already full of pieces' do
6.times { $connect_four.board.add_game_piece_to(0, "\u2468") }
expect(output.string).to include("The column you chose is fully occupied.")
end
let(:input) { StringIO.new("!\n") }
it 'notifies the user their input is invalid when a non-numeric string is entered' do
$connect_four.get_col_choice(input, output)
expect(output.string).to include("Your choice of column is invalid. Try again.")
end
context 'column choice is out of bounds' do
let(:input) { StringIO.new("7\n") }
it 'notifies the user their column choice is out of bounds when col is greater than 6' do
$connect_four.get_col_choice(input, output)
expect(output.string).to include("The column you chose is out of bounds.")
end
let(:input) { StringIO.new("-1\n") }
it 'notifies the user their column choice is out of bounds when col is less than 0' do
$connect_four.get_col_choice(input, output)
expect(output.string).to include("The column number you chose is out of bounds.")
end
end
end

I was able to solve my problem by removing the input and output from get_col_choice:
def get_col_choice
print "Player #{#player}, choose a column from 0-6: "
input_string = gets.chomp
begin
col_choice = Integer(input_string)
raise("The column you chose is out of bounds.") if out_of_bounds?(col_choice)
raise("The column you chose is fully occupied.") if #board.unavailable?(col_choice)
return col_choice
rescue TypeError, ArgumentError
puts "Your choice of column is invalid. Try again."
rescue RuntimeError => err
puts err.message
end
get_col_choice
end
In my rspec file, instead of using StringIO to inject the behavior I expected into get_col_choice, I stubbed gets to return the values I needed to trigger expected behaviors:
#code left out for brevity
before(:each) do
#game = ConnectFourGame.new
end
describe '#get_col_choice' do
before(:each) do
allow($stdout).to receive(:write)
#game.player = 1
end
context 'valid input' do
before(:each) do
allow(#game).to receive(:gets) {"0\n"}
end
it 'notifies users which player gets to choose a column' do
expect{ #game.get_col_choice }.to output('Player 1, choose a column from 0-6: ').to_stdout
end
it "returns the user's column choice as an integer" do
expect(#game.get_col_choice).to eq(0)
end
end
context 'invalid input' do
output_expectation = Proc.new{ expect { #game.get_col_choice }.to output(output).to_stdout }
it 'notifies the user if the column they chose is already full of pieces' do
allow(#game).to receive(:gets).and_return("0\n", "1\n")
6.times { #game.board.add_game_piece_to(0, "\u2648") }
output = "Player 1, choose a column from 0-6: The column you chose is fully occupied.\nPlayer 1, choose a column from 0-6: "
output_expectation
end
it 'notifies the user their input is invalid when a non-numeric string is entered' do
allow(#game).to receive(:gets).and_return("foo\n", "0\n")
output = "Player 1, choose a column from 0-6: Your choice of column is invalid. Try again.\nPlayer 1, choose a column from 0-6: "
output_expectation
end
it 'notifies the user their column choice is out of bounds when col is greater than 6' do
allow(#game).to receive(:gets).and_return("7\n", "0\n")
output = "Player 1, choose a column from 0-6: The column you chose is out of bounds.\nPlayer 1, choose a column from 0-6: "
output_expectation
end
it 'notifies the user their column choice is out of bounds when col is less than 0' do
allow(#game).to receive(:gets).and_return("-1\n", "0\n")
output = "Player 1, choose a column from 0-6: The column you chose is out of bounds.\nPlayer 1, choose a column from 0-6: "
output_expectation
end
end
end
The reason I received errors in my previous implementation of get_col_choice was because after StringIO received my first input string, it closed, resulting in a value of nil that I tried to call gets on.

Related

RSpec and active record validations

I'm trying to validate that the rating of a movie is greater than 0 and less than or equal to 5 and for that I'm using the "be_valid" in RSpec which seems to work when I check if the movie title is nil but it's not working for the rating.
I don't understand why
Model:
class Movie < ApplicationRecord
validates :title, presence: true
validates :rating, presence: true, numericality: { greater_than_or_equal_to: 0,less_than_or_equal_to: 5, only_integer: true }
end
Spec:
RSpec.describe Movie, type: :model do
# checking model validations
subject{described_class.new}
it "title must be present" do
subject.title = ""
expect(subject).not_to be_valid
end
it "rating must be greater than 0" do
subject.rating = 1
expect(subject.rating).to be_valid
end
it "rating must be less than or equal to 5" do
subject.rating = 5
expect(subject.rating).to be_valid
end
end
Error:
Movie
title must be present
rating must be greater than 0 (FAILED - 1)
rating must be less than or equal to 5 (FAILED - 2)
Failures:
1) Movie rating must be greater than 0
Failure/Error: expect(subject.rating).to be_valid
NoMethodError:
undefined method `valid?' for 1:Integer
# ./spec/models/movie_spec.rb:15:in `block (2 levels) in <top (required)>'
2) Movie rating must be less than or equal to 5
Failure/Error: expect(rating).to be_valid
NameError:
undefined local variable or method `rating' for #<RSpec::ExampleGroups::Movie:0x00007f8332f46fc0>
# ./spec/models/movie_spec.rb:20:in `block (2 levels) in <top (required)>'
You should use expect(subject).to be_valid in the other 2 test cases. You are getting the error because you are trying to validate subject.rating which is an integer.

CSV Import RSpec error in Ruby

I am working on an assignment in Ruby.. I had to write RSPEC tests for a method that removes an entry from my address_book app.. and also had to write a test and a method for importing 5 entries from a CSV. In my remove_entry method when I run the Specs it says that I have an undefined method of delete. I have already asked a couple fellow ruby devs and they could not figure it out at first glance. The next error is that when I run the tests for my CSV imports.. The data is not being imported in the proper order. I have spent several hours walking through my code over and over and researching in attempts to fix this.. I am at my wits end.. Any help would be appreciated!
address_book.rb
require_relative 'entry'
require "csv"
class AddressBook
attr_accessor :entries
def initialize
#entries = []
end
def add_entry(name,phone_number,email)
index = 0
#entries.each do |entry|
if name < entry.name
break
end
index += 1
end
#entries.insert(index, Entry.new(name, phone_number, email))
end
def import_from_csv(file_name)
csv_text = File.read(file_name)
csv = CSV.parse(csv_text, headers: true, skip_blanks: true)
csv.each do |row|
row_hash = row.to_hash
add_entry(row_hash["name"], row_hash["phone_number"], row_hash["email"])
#can you clarify what the above is doing
#is the format of row_hash["name"] because it is iterating over a hash or because it is an array?
end
end
def remove_entry(name,phone_number,email)
#entries.each do |entry|
if (name == entry.name) && (email == entry.email) && (phone_number = entry.phone_number)
entry.delete #this line returns an error in my RSPEC test
else
p "Entry does not exist \n Please try again."
end
end
end
end
address_book_spec.rb
require_relative "../models/address_book"
RSpec.describe AddressBook do
let(:book) {AddressBook.new} # => lets us use the book variable in every test
describe "attributes" do
it "should respond to entries" do
# book = AddressBook.new # => Replaced by line 4
expect(book).to respond_to(:entries)
end
it "should initialize entries as an array" do
# book = AddressBook.new # => Replaced by line 4
expect(book.entries).to be_a(Array)
end
it "should initialize entries as an empty array" do
# book = AddressBook.new # => Replaced by line 4
expect(book.entries.size).to eq(0)
end
end
describe "#add_entry" do
it "adds only a single entry to the Address Book" do
# book = AddressBook.new # => Replaced by line 4
book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
expect(book.entries.size).to eq(1)
end
it "adds the correct information to entries" do
# book = AddressBook.new # => Replaced by line 4
book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
new_entry = book.entries[0]
expect(new_entry.name).to eq('Ada Lovelace')
expect(new_entry.phone_number).to eq('010.012.1815')
expect(new_entry.email).to eq('augusta.king#lovelace.com')
end
end
# added remove entry test
describe "#remove_entry" do
it "should remove a single entry" do
# book = AddressBook.new # => Replaced by line 4
book.add_entry('Austin Thesing', '800.445.8833','austin#thesing.xyz')
expect(book.entries.size).to eq(1)
book.remove_entry('Austin Thesing', '800.445.8833','austin#thesing.xyz')
expect(book.entries.size).to eq(0)
end
end
def check_entry(entry,expected_name,expected_phone_number, expected_email)
expect(entry.name).to eql(expected_name)
expect(entry.phone_number).to eql(expected_phone_number)
expect(entry.email).to eql(expected_email)
end
describe "#import_from_csv" do
it "import an entry from a CSV file" do
book.import_from_csv("entries.csv")
book_size = book.entries.size
expect(book_size).to eq 5 #checks the size of the book
end
it "adds the first entry" do
book.import_from_csv("entries.csv")
entry_one = book.entries[0]
check_entry(entry_one,"Mark Griffo","123456789","mark#bloc.com")
end
it "adds the second entry" do
book.import_from_csv("entries.csv")
entry_two = book.entries[1]
check_entry(entry_two,"Natalie Griffo","123456789","natalie#bloc.com")
end
it "adds the third entry" do
book.import_from_csv("entries.csv")
entry_three = book.entries[2]
check_entry(entry_three, "Steve Thesing", "8583878899", "steve#steve.com")
end
it "adds the fourth entry" do
book.import_from_csv("entries.csv")
entry_four = book.entries[3]
check_entry(entry_four, "Haidee Thesing", "8584458833", "h#thesing.com")
end
it "adds the fifth entry" do
book.import_from_csv("entries.csv")
entry_five = book.entries[4]
check_entry(entry_five, "Olivia Meers", "0987654321", "olivia#meers.com")
end
end
end
Terminal Output/Spec Failures
Austins-MacBook-Pro:address-bloc austinthesing$ rspec spec/address_book_spec.rb
.....F.FFFFF
Failures:
1) AddressBook#remove_entry should remove a single entry
Failure/Error: book.remove_entry('Austin Thesing', '800.445.8833','austin#thesing.xyz')
NoMethodError:
undefined method `delete' for #<Entry:0x007f8e8c1dea08>
# ./models/address_book.rb:37:in `block in remove_entry'
# ./models/address_book.rb:35:in `each'
# ./models/address_book.rb:35:in `remove_entry'
# ./spec/address_book_spec.rb:45:in `block (3 levels) in <top (required)>'
2) AddressBook#import_from_csv adds the first entry
Failure/Error: expect(entry.name).to eql(expected_name)
expected: "Mark Griffo"
got: "Haidee Thesing"
(compared using eql?)
# ./spec/address_book_spec.rb:50:in `check_entry'
# ./spec/address_book_spec.rb:64:in `block (3 levels) in <top (required)>'
3) AddressBook#import_from_csv adds the second entry
Failure/Error: expect(entry.name).to eql(expected_name)
expected: "Natalie Griffo"
got: "Mark Griffo"
(compared using eql?)
# ./spec/address_book_spec.rb:50:in `check_entry'
# ./spec/address_book_spec.rb:69:in `block (3 levels) in <top (required)>'
4) AddressBook#import_from_csv adds the third entry
Failure/Error: expect(entry.name).to eql(expected_name)
expected: "Steve Thesing"
got: "Natalie Griffo"
(compared using eql?)
# ./spec/address_book_spec.rb:50:in `check_entry'
# ./spec/address_book_spec.rb:74:in `block (3 levels) in <top (required)>'
5) AddressBook#import_from_csv adds the fourth entry
Failure/Error: expect(entry.name).to eql(expected_name)
expected: "Haidee Thesing"
got: "Olivia Meers"
(compared using eql?)
# ./spec/address_book_spec.rb:50:in `check_entry'
# ./spec/address_book_spec.rb:79:in `block (3 levels) in <top (required)>'
6) AddressBook#import_from_csv adds the fifth entry
Failure/Error: expect(entry.name).to eql(expected_name)
expected: "Olivia Meers"
got: "Steve Thesing"
(compared using eql?)
# ./spec/address_book_spec.rb:50:in `check_entry'
# ./spec/address_book_spec.rb:84:in `block (3 levels) in <top (required)>'
Finished in 0.0176 seconds (files took 0.08714 seconds to load)
12 examples, 6 failures
Failed examples:
rspec ./spec/address_book_spec.rb:40 # AddressBook#remove_entry should remove a single entry
rspec ./spec/address_book_spec.rb:61 # AddressBook#import_from_csv adds the first entry
rspec ./spec/address_book_spec.rb:66 # AddressBook#import_from_csv adds the second entry
rspec ./spec/address_book_spec.rb:71 # AddressBook#import_from_csv adds the third entry
rspec ./spec/address_book_spec.rb:76 # AddressBook#import_from_csv adds the fourth entry
rspec ./spec/address_book_spec.rb:81 # AddressBook#import_from_csv adds the fifth entry
First test is failing because the delete method is being used incorrectly:
entry.delete #this line returns an error in my RSPEC test
needs to be
#entries.delete(entry)
Entries are being inserted in a scrambled fashion because you are using break where you probably want
next
(I suppose that loop is supposed to insert in alphabetical order.)
break command terminates the full each block, so no more iterations are executed if there is one name of higher alphabetical order, next just skips to the next iteration.
That should be it

Rspec 3 vs Rspec 2 matchers

Learning how to Rspec 3. I have a question on the matchers. The tutorial i am following is based on Rspec 2.
describe Team do
it "has a name" do
#Team.new("Random name").should respond_to :name
expect { Team.new("Random name") }.to be(:name)
end
it "has a list of players" do
#Team.new("Random name").players.should be_kind_of Array
expect { Team.new("Random name").players }.to be_kind_of(Array)
end
end
Why is the code causing an error while the one i commented out passing with depreciation warning.
Error
Failures:
1) Team has a name
Failure/Error: expect { Team.new("Random name") }.to be(:name)
You must pass an argument rather than a block to use the provided matcher (equal :name), or the matcher must implement `supports_block_expectations?`.
# ./spec/team_spec.rb:7:in `block (2 levels) in <top (required)>'
2) Team has a list of players
Failure/Error: expect { Team.new("Random name").players }.to be_kind_of(Array)
You must pass an argument rather than a block to use the provided matcher (be a kind of Array), or the matcher must implement `supports_block_expectations?`.
# ./spec/team_spec.rb:13:in `block (2 levels) in <top (required)>'
You should use normal brackets for those tests:
expect(Team.new("Random name")).to eq :name
When you use curly brackets, you are passing a block of code. For rspec3 it means that you will put some expectations about the execution of this block rather than on the result of execution, so for example
expect { raise 'hello' }.to raise_error
EDIT:
Note however that this test will fail, as Team.new returns an object, not a symbol. You can modify your test so it passes:
expect(Team.new("Random name")).to respond_to :name
# or
expect(Team.new("Random name").name).to eq "Random name"

"ArgumentError: wrong number of arguments (2 for 0)" though method requires two parameters

I have a MiniTest like this:
describe Message do
describe "#is_getting_unavailable" do
let( :message ) { Message.new() }
it "should be false when user does not exist in the database" do
message.handle
assert_equal(false, message.is_getting_unavailable)
end
end
end
Running this gives me complaint from assert_equal:
Message::#is_getting_unavailable#test_0001_should be false when user does not exist in the database
ArgumentError: wrong number of arguments (2 for 0)
test/unit/message_test.rb:148:in `(root)'
org/jruby/RubyBasicObject.java:1703:in `__send__'
org/jruby/RubyKernel.java:2209:in `send'
org/jruby/RubyArray.java:1617:in `each'
org/jruby/RubyArray.java:1617:in `each'
I did not understand this, so I included the test (just before the call of assert_equal):
puts method(:assert_equal).inspect
puts method(:assert_equal).arity
puts method(:assert_equal).source_location.inspect
The output is:
#<Method: #<Class:0x1d1e394d>(Minitest::Assertions)#assert_equal>
-3
["/home/rjung/.rvm/gems/jruby-1.7.4/gems/minitest-5.0.6/lib/minitest/assertions.rb", 155]
So the method is correct, and the arity is correct. What's the issue here?
We also use rr, timecop. Any other questions, that could help me find a solution?
It took me a while, but I could narrow the problem down to this failing test:
require 'minitest/autorun'
describe 'Message' do
let( :message ) { Hash.new }
it "should not fail awkwardly" do
assert_equal false, message.nil?
end
end
The output of this test is
1) Error:
Message#test_0001_should not fail awkwardly:
ArgumentError: wrong number of arguments (2 for 0)
test/unit/message_test.rb:7:in `(root)'
org/jruby/RubyBasicObject.java:1703:in `__send__'
org/jruby/RubyKernel.java:2209:in `send'
org/jruby/RubyArray.java:1617:in `each'
org/jruby/RubyArray.java:1617:in `each'
So I filed a Bug for https://github.com/seattlerb/minitest/issues/343.
Minitest does have a method message that was overwritten, so don't use message as a variable.
What I still wonder is, why the stacktrace says the wrong number of arguments happen in message_test.rb:7, because the method that takes no arguments (message) is definitive called from somewhere else.

rspec - wrong number of arguments (0 for 1..2)

Given this code:
class Game
def self.game_board
return [[][][]]
end
def self.empty_output
'_|_|_'+
'_|_|_'+
' | |'
end
end
and these tests:
describe 'It should display a grid' do
it 'should have empty output' do
Game.empty_output.should ==
'_|_|_'+
'_|_|_'+
' | |'
end
it 'should have an empty array for the game ' do
Game.game_board.should ==
[[][][]]
end
end
why does the test fail that tries to return the array of arrays with the error
.F
Failures:
1) "It should display a grid should be empty
Failure/Error: return [[][][]]
ArgumentError:
wrong number of arguments (0 for 1..2)
# ./checkers_rspec.rb:4:in `[]'
# ./checkers_rspec.rb:4:in `game_board'
# ./checkers_rspec.rb:24:in `block (2 levels) in <top (required)>'
Finished in 0.00121 seconds
2 examples, 1 failure
[[][][]] should hae been [[],[],[]] in both cases.

Resources