Search code examples
rubyunit-testingminitestassertraises

Ruby - Testing a method that calls itself in Minitest


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


Solution

  • 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