Reputation: 307
I'm new to both Ruby and RSpec, and have already spent hours trying to write the first steps for testing an interactive tic tac toe program, but haven't located any useful answers to the error I'm getting:
> bundle exec rspec --format documentation
Do you want to play tic-tac-toe? (y/n)
An error occurred while loading ./spec/tic_tac_toe_spec.rb.
Failure/Error: choice = gets.chomp.downcase
Errno::ENOENT:
No such file or directory @ rb_sysopen - --format
# ./lib/tic_tac_toe.rb:70:in `gets'
# ./lib/tic_tac_toe.rb:70:in `gets'
# ./lib/tic_tac_toe.rb:70:in `start_game'
# ./lib/tic_tac_toe.rb:144:in `<top (required)>'
# ./spec/tic_tac_toe_spec.rb:1:in `require'
# ./spec/tic_tac_toe_spec.rb:1:in `<top (required)>'
No examples found.
Finished in 0.0004 seconds (files took 0.08415 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples
When you start the game, it prompts whether you want to play (y/n). I've already attempted different variations of using mocking and stubs but always get this same error. I imagine I'm missing something very basic but cannot see it.
Question: how would one adequately address (using RSpec) this first prompt of the game?
The game uses an independent method (start_game
) but not a class to start. I'm looking for some way mock or stub this with some default input (like y
or n
) as I want the tests to be automated. All the examples I see use a class to start a program, however (rather than a stand-alone method like here).
Currently in spec/tic_tac_toe_spec.rb
there are some let
keywords for :output
and :game
which I found from an example somewhere but obviously this doesn't work.
Edit I want to test the following method. The RSpec code keeps choking on the choice = gets.chomp.downcase
line.
def start_game
puts "Do you want to play tic-tac-toe? (y/n)"
choice = gets.chomp.downcase
unless ["y","n"].include?(choice)
abort("Please answer with y or n.")
end
case choice
when "y"
puts "Ok, let's start!"
b = Board.new
puts "Enter name of player 1 (O)"
player1name = gets.chomp
player1 = Player.new(player1name)
puts "Now enter name of player 2 (X)"
player2name = gets.chomp
player2 = Player.new(player2name)
play_game(player1, player2, b)
when "n"
puts "Your loss."
end
end #start_game
Upvotes: 1
Views: 1128
Reputation: 1691
You are receiving the error because you are passing the --format documentation
as a parameter to RSpec.
gets
reads the ARGV that have been passed. This also includes the RSpec parameters. You should use STDIN.gets
so that you only read standard input and not the parameters.
You can read more about in this question: https://stackoverflow.com/a/19799223/6156030
Upvotes: 3
Reputation: 28285
A simple approach could be:
it 'works' do
allow($stdin).to receive(:gets).and_return('y')
expect { start_game } # (Or however you run it!)
.to output("Ok, let's start!")
.to_stdout
end
You can also specify multiple return values for gets
, which will be used sequentially as the test runs.
The alternative approach you seem to have started (but not fully implemented) is to inject an explicit output
(and, presumably, one day an input
) object into the game.
This would indeed be a cleaner approach from the purist's point of view - i.e. not stubbing the global objects like $stdin
- but is probably overkill for your initial version of the code. Unless you plan doing something fancy like running parallel specs, I wouldn't worry about that.
Edit: After looking at your actual code in more detail, I see the problem. You're defining global methods which are doing multiple things and are tightly coupled. This makes writing tests much harder!
Here I have added a working test example:
https://github.com/tom-lord/tic_tac_toe_rspec/commit/840df0b7f1380296db97feff0cd3ca995c5c6ee3
However, going forward in order to simplify this my advice would be to define all each method within an appropriate class, and make the code less procedural. (i.e. Don't just make the end of one method call the next method, in a long sequence!) This refactoring is perhaps beyond the scope of a StackOverflow answer though.
Upvotes: 1
Reputation: 15992
You need to stub and mock gets
method in your specs:
yes_gets = double("yes_gets")
allow($stdin).to receive(:gets).and_return(yes_gets)
Which then you can make it respond to #chomp
:
expect(yes_gets).to receive(:chomp).and_return('Y')
You can cover the similar method call for downcase
by returning this double object itself.
You can also do the similar work for mock object for your 'N'
case where you'd expect game to exit when player inputs an N(No):
no_gets = double("no_gets")
allow($stdin).to receive(:gets).and_return(no_gets)
expect(no_gets).to receive(:chomp).and_return('N')
Upvotes: 0