Ruby - Testing a method that calls itself in Minitest

172 Views Asked by At

I'm having trouble developing unit tests for a method that calls itself (a game loop) in Ruby using minitest. What I've attempted has been stubbing the method I'm trying to call in said game loop with my input. Here's the game loop:

#main game loop
 def playRound
  #draw board
  @board.printBoard
  #get input
  playerInput = gets.chomp #returns user input without ending newline
 
  #interpret input, quitting or beginning set selection for a player
     case playerInput
      when "q"
       quit
      when "a", "l"
        set = getPlayerSet()
       if(playerInput == "a")
        player = 1
       else
        player = 2
       end
      when "h"
       if @hintsEnabled
        giveHint
        playRound
       else
        puts "Hints are disabled"
        playRound
       end
      else
       puts "Input not recognized."
     end
     if(set != nil)
      #have board test set
      checkSet(set, player)
     end
     #check if player has quitted or there are no more valid sets
     unless @quitted || @board.boardComplete
      playRound
     end
 end

Much of it is ultimately irrelevant, all I'm trying to test is that this switch statement is calling the correct methods. Currently I'm trying to circumvent the loop by stubbing the called method to raise an error (which my test assers_raise's):

def test_playRound_a_input_triggers_getPlayerSet
  @game.stub :getPlayerSet, raise(StandardError) do
   assert_raises(StandardError) do
    simulate_stdin("") { 
      @game.playRound
     }
   end
   end
 end

This approach does not seem to work, however, as Minitest is recording the results of the above test as an error with the message

E
Error:
TestGame#test_playRound_a_input_triggers_getPlayerSet:
StandardError: StandardError
test_game.rb:136:in `test_playRound_a_input_triggers_getPlayerSet'

If anyone has any advice or direction for me it would be massively appreciated as I can't tell what's going wrong

1

There are 1 best solutions below

4
On BEST ANSWER

I'm not very familiar with minitest, but I expect you need to wrap the raise(exception) in a block, otherwise your test code is raising the exception immediately in your test (not as a result of the stubbed method being called).

Something like:

class CustomTestError < RuntimeError; end
def test_playRound_a_input_triggers_getPlayerSet
  raise_error = -> { raise(CustomTestError) }
  @game.stub(:getPlayerSet, raise_error) do
    assert_raises(CustomTestError) do
      simulate_stdin("") { 
        @game.playRound
      }
    end
  end
end

-- EDIT --

Sometimes when i'm having difficulty testing a method it's a sign that I should refactor things to be easier to test (and thus have a cleaner, simpler interface, possibly be easier to understand later).

I don't code games and don't know what's typical for a game loop, but that method looks very difficult to test. I'd try to break it into a couple steps where each step/command can be easily tested in isolation. One option for this would be to define a method for each command and use send. This would allow you to test that each command works separately from your input parsing and separately from the game loop itself.

  COMMANDS = {
    q: :quit,
    # etc..
  }.stringify_keys.freeze

  def play_round # Ruby methods should be snake_case rather than camelCase
    @board.print_board
    run_command(gets.chomp)
    play_round unless @quitted || @board.board_complete
  end

  def run_command(input)
    command = parse_input_to_command(input)
    run_command(command)
  end

  def parse_input_to_command(input)
    COMMANDS[input] || :bad_command
  end
  def run_command(command)
    send("run_#{command}")
  end
  # Then a method for each command, e.g.
  def run_bad_input
    puts "Input not recognized"
  end

However, for this type of problem I really like a functional approach, where each command is just a stateless function that you pass state into and get new state back. These could either mutate their input state (eww) or return a new copy of the board with updated state (yay!). Something like:

  COMMANDS = {
    # All state change must be done on board. To be a functional pattern, you should not mutate the board but return a new one. For this I invent a `.copy()` method that takes attributes to update as input.
    q: -> {|board| board.copy(quitted: true) },
    h: -> HintGiver.new, # If these commands were complex, they could live in a separate class entirely.
    bad_command: -> {|board| puts "Unrecognized command"; board },
    #
  }.stringify_keys.freeze
  def play_round 
    @board.print_board
    command = parse_input_to_command(gets.chomp)
    @board = command.call(@board)
    play_round unless @board.quitted || @board.board_complete
  end

  def parse_input_to_command(input)
    COMMANDS[input] || COMMANDS[:bad_command]
  end