• Ruby Version 2.7.2
  • Rspec Version 3.12.0

So I'm currently working through App Academy Open and I'm at the point where we're creating a tic tac toe game. I've written out all my tests and they pass except for the last few.

I have successfully stubbed method calls in the past but for whatever reason, I'm not getting it to work here.

I have 3 classes Board, HumanPlayer, and Game. The method I am currently testing is #play from within the Gameclass:

def play
  while @board.empty_positions?
    puts @board.print
    position = @current_player.get_position
    @board.place_mark(position, @current_player.mark)

    if @board.win?(@current_player.mark)
      puts "Player #{@current_player.mark} wins!"
      return
    else
      switch_turn
    end
  end

  puts "The game ended in a draw!"
end

Here is what my test looks like:

RSpec.describe Game do
  let(:game) { Game.new(:X, :O) }
  
  # ...

  describe "#play" do
    before :each do
      @board = game.instance_variable_get(:@board)
      @board.place_mark([0, 0], :X)
      @board.place_mark([0, 1], :O)
      @board.place_mark([0, 2], :X)
      @board.place_mark([1, 0], :O)
      @board.place_mark([2, 0], :X)
      @board.place_mark([1, 1], :O)
      @board.place_mark([2, 2], :X)
    end

    it "should call Board#place_mark" do
      @current_player = game.instance_variable_get(:@current_player)
      allow(@current_player).to receive(:get_position).and_return([1, 2])

      expect(@board).to receive(:place_mark)
      game.play
    end
  end
end

Here is the HumanPlayer#get_position method:

def get_position
  puts "Player #{@mark}, enter two numbers representing a position in the format `row col`"

  position = gets.chomp.split(" ")

  if position.length != 2 || # not 2 characters
      position.any? { |n| n.to_i.to_s != n } # not all numeric

    raise "Invalid Position"
  end

  position.map(&:to_i)
end

Here is the Board#place_mark method:

def place_mark(position, mark)
  raise "Placement Invalid" if !valid?(position) || !empty?(position)

  row = position[0]
  col = position[1]

  @grid[row][col] = mark
end

So whenever I run the tests I always get the error:

Game Instance Methods #play should call Board#place_mark
Failure/Error: position = gets.chomp.split(" ")
     
Errno::ENOENT:
  No such file or directory @ rb_sysopen - spec/2_game_spec.rb:85
# ./lib/human_player.rb:11:in `gets'
# ./lib/human_player.rb:11:in `gets'
# ./lib/human_player.rb:11:in `get_position'
# ./lib/game.rb:20:in `play'
# ./spec/2_game_spec.rb:92:in `block (4 levels) in <top (required)>'

I believe I'm stubbing the HumanPlayer.get_position method to return [1, 2] when called but for whatever reason, the Board.place_mark method does not successfully place the piece on the board and thus, the HumanPlayer.get_position gets called again because of the loop and when it hits the gets call, it produces that error output.

I've tried stubbing the gets call with this:

it "should call Board#place_mark" do
  @current_player = game.instance_variable_get(:@current_player)
  allow(@current_player).to receive(:gets).and_return("1 2")

  expect(@board).to receive(:place_mark)
  game.play
end

I also tired allow_any_instance_of(HumanPlayer) but it just prints the board in an endless loop:

it "should call Board#place_mark" do
  @current_player = game.instance_variable_get(:@current_player)
  allow_any_instance_of(HumanPlayer).to receive(:get_position).and_return([1, 2])

  expect(@board).to receive(:place_mark)
  game.play
end

This is my first question on SO, so if there is anything I need to add please let me know. Thanks in advance.

1

There are 1 best solutions below

1
Jared Beck On

If you can't figure out the Errno::ENOENT error, you might try a slightly different design, which I think is easier to stub.

class GameCLI
  def gets
    Kernel.gets
  end
end

class HumanPlayer
  def get_position
    # ..
    cli = GameCLI.new # or, pass cli instance as argument to e.g. get_position
    position = cli.gets # ..
    # ..
  end
end

RSpec.describe HumanPlayer do
  describe '#get_position' do
    it '..' do
      allow_any_instance_of(GameCLI).to receive(:gets).and_return('..')
      # ..
    end
  end
end