You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RSpec测试中方法Stub失效触发Errno::ENOENT报错的问题咨询

环境信息
  • Ruby版本:2.7.2
  • Rspec版本:3.12.0

问题场景

我在学习App Academy Open的井字棋项目,大部分测试已通过,但最后几个关于Game#play方法的测试卡壳了。之前成功Stub过方法调用,但这次Stub完全没生效。

核心代码片段

Game类的#play方法

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

测试代码

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

HumanPlayer的#get_position方法

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

Board的#place_mark方法

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

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

  @grid[row][col] = mark
end

报错信息
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)>'

我原本以为Stub了HumanPlayer#get_position返回[1,2]就能跳过真实的gets调用,但实际Board#place_mark好像没生效,导致循环再次调用get_position,触发gets时报错。


尝试过的无效方案
  1. Stub gets方法:
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
  1. 使用allow_any_instance_of(HumanPlayer):
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

这个方案导致棋盘无限打印循环。


解决方案

问题根源

  1. Stub对象错误:before块里下了7步棋(X走4次、O走3次),此时游戏的当前玩家是O,但你Stub的是游戏初始化时的X玩家,实际调用get_position的是未被Stub的O玩家实例,触发了真实的gets方法。
  2. 无限循环原因:用allow_any_instance_of时,每次调用get_position都返回[1,2],第一次落子后该位置已被占据,place_mark抛出异常,循环继续重复调用同一个无效位置,导致无限循环。

修复后的测试代码

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)
    # 获取当前真实的玩家(此时是O)
    @current_player = game.instance_variable_get(:@current_player)
    # 屏蔽控制台打印,避免测试输出混乱
    allow(@board).to receive(:print).and_return("mock board")
    allow($stdout).to receive(:puts)
  end

  it "should call Board#place_mark and end the game with O's win" do
    # Stub正确的当前玩家实例
    allow(@current_player).to receive(:get_position).and_return([1, 2])
    # 明确期望place_mark的调用参数
    expect(@board).to receive(:place_mark).with([1, 2], :O)
    # 验证游戏会因O获胜终止
    expect { game.play }.to output(/Player O wins!/).to_stdout
  end
end

额外说明

  • 落子[1,2]后,O的第1行形成O-O-O的获胜线,游戏会直接终止,不会进入下一轮循环。
  • 屏蔽puts和board.print的输出,能让测试结果更清晰,避免无关信息干扰。

内容的提问来源于stack exchange,提问作者Abdul

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 18:55:33