orlando21
orlando21

Reputation: 307

RSpec: Using a method stub to test interactive user input

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

Answers (3)

Laura Paakkinen
Laura Paakkinen

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

Tom Lord
Tom Lord

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

Surya
Surya

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

Related Questions