Search code examples
rubytestingrspectdd

Rspec test fails


I am trying to test #game_over? method in which it calls #check_row in Board class using an instance_double but the test fails. Here is the test:

describe '#game_over' do
    subject(:game_over) { described_class.new }
    let(:board) { instance_double(Board) }
    context 'when #game_over is called' do
      it 'calls #check_row in Board' do
        game_over.game_over?
        expect(board).to receive(:check_row)
        #game_over.game_over?
      end
    end
  end

I was expecting the #game_over? to call #check_row in Board class but the test fails. Here is the method I am testing:

 def game_over?
    return true if @board.check_row || @board.check_column || @board.check_diagonal || @board.check_antidiagonal

    false
  end

Here is the failure message:

  1) Game#game_over when #game_over is called calls #check_row in Board
     Failure/Error: expect(board).to receive(:check_row)
     
       (InstanceDouble(Board) (anonymous)).check_row(*(any args))
           expected: 1 time with any arguments
           received: 0 times with any arguments

Here is my Game #initialize method:

  def initialize
    @board = Board.new
  end

Solution

  • The instance of Board in your Game class and the board mock in your test are different instances and therefore the test fails.

    I suggest using dependency injection to be able to control the board instance in the Game and change your initializer and your test like this:

    # in the Game class
    def initialize(board = nil)
      @board = board || Board.new
    end
    
    # in your spec
    describe '#game_over?' do
      subject(:game) { described_class.new(board) } # inject the board stub here
      
      let(:board) { instance_double(Board) }
    
      before { allow(board).to receive(:check_row).and_return(true) } 
    
      it 'delegates to Board#check_row' do
        game.game_over?
        
        expect(board).to have_received(:check_row)
      end
    end
    

    Note:

    I would argue that the test in its current form doesn't add much value and that it tests an internal implementation detail that you should not really care about.

    There is no benefit in testing that a specific method is called on an internal @board object in the Game. In fact, testing such internal behavior will make it more difficult to refactor the code later on when requirements changed or new features are implemented.

    Instead, I suggest focusing on testing that the method returns the expected result under certain preconditions (but not if and how the result is received from another object).

    In this example, I suggest not testing that board.check_row was called, but that Board#game_over? returns the expected result because of the call) like this:

    # in the Game class
    def initialize(board = nil)
      @board = board || Board.new
    end
    
    # in your spec
    describe '#game_over?' do
      subject(:game) { described_class.new(board) }    # inject the board stub here
      
      let(:board) { instance_double(Board) }
    
      context 'with the board row check returns true' do
        before { allow(board).to receive(:check_row).and_return(true) }
    
        it 'is game over' do
          expect(game).to be_game_over
        end
      end
    end