Ruby MiniTest:在无限循环中测试运行

rpppsulh  于 2023-04-29  发布在  Ruby
关注(0)|答案(1)|浏览(112)

我想用MiniTest测试下面的代码。代码测试用户是否提供了有效的输入。

正在测试的代码:

def player_input(min, max)
  loop do
    user_input = gets.chomp
    verified_number = verify_input(min, max, user_input.to_i) if user_input.match?(/^\d+$/)
    return verified_number if verified_number
    puts "Input error! Please enter a number between #{min} or #{max}."
  end
end

def verify_input(min, max, input)
  return input if input.between?(min, max)
end

**MiniTest代码:**测试代码使用无效输入对:gets方法进行存根。然后Assertplayer_input方法的输出是错误消息。

def test_when_user_inputs_an_incorrect_value_runs_loop_once
  invalid_input = '11'
  @game_input.stub(:gets, invalid_input) do
    min = @game_input.instance_variable_get(:@minimum)
    max = @game_input.instance_variable_get(:@maximum)
    error_message = "Input error! Please enter a number between #{min} or #{max}."
    assert_output(error_message, "") { @game_input.player_input(min, max) }
  end
end

**问题:**问题是因为我把测试在无限循环中运行的:gets方法截断了。如何在MiniTest中测试获取用户输入的简单循环?

谢谢
我尝试将代码更改为STDERR。puts以确保代码返回一个可以Assert的标准错误。

ejk8hzay

ejk8hzay1#

未来为什么要先测试

这类问题就是为什么你应该首先编写测试。测试为测试而设计的代码要容易得多,而不是在事后改造代码或测试框架。从中吸取教训。
最简单的事情可能会起作用
不需要修复代码或测试,只要Assert如果由于任何原因陷入循环,就会从标准库的Timeout模块中获得Timeout::Error异常。例如:

require 'timeout'

assert_raises(Timeout::Error) do
  Timeout::timeout(0.1) { @game_input.player_input(min, max) }
end

假设这不是验证特定的错误消息,它解决了您在循环中面临的实际问题,而不需要更改现有的逻辑。如果你只是想停止在这个特定的测试中被挂起,而不在乎为什么,那么就走这条路。

让事情更简单

假设你想修复你的代码,而不仅仅是避免陷入循环,你应该简化你的代码。它只是看起来像一个天生的坏主意存根内核#获取,即使你做一个块内。MiniTest允许您将存根的范围限定为块,但有一种更简单的方法可以测试现有代码,而不会扭曲自己:只需添加一个方法参数,就可以让您在测试过程中做一些不同的事情!
你没有展示你是如何示例化你的类的,而且通常来说,为多个测试重用一个修改过的对象是一个坏主意,因为这会创建有状态的和顺序相关的测试。因此,您可以示例化类的一个新示例,然后在ENV['APP_ENV'] == 'test'和您传递了一些显式测试参数时,让方法执行不同的操作。考虑以下情况:

def player_input(min, max, test_input: nil)
  loop do
    user_input = ENV['APP_ENV'].eql?('test') ? test_input : gets.chomp
    verified_number = verify_input(min, max, user_input.to_i) if user_input.match?(/^\d+$/)
    return verified_number if verified_number
    puts "Input error! Please enter a number between #{min} or #{max}."
  end
end

接下来,如果您的测试环境没有为您设置ENV['APP_ENV'] = 'test',请确保您的测试套件设置了ENV['APP_ENV'] = 'test'。你可能也应该让GameInput公开测试变量的访问器,而不是尝试使用#instance_variable_get。最后,只需调用#player_input,并为测试值添加一个关键字参数。例如,考虑以下框架,您可能需要根据未共享的代码部分对其进行调整。

# add the accessor directly to your class,
# or reopen the class & add a read accessor
# in your test suite; the first option is
# better
class GetInput
  attr_reader :min, :max
end

# get a clean copy of your class for the test
def test_when_user_inputs_an_incorrect_value_runs_loop_once
  # this should really be in your environment,
  # but you could set it inside your test setup
  # if you really need to
  ENV['APP_ENV'] ||= 'test'

  game_input = GameInput.new
  game_input.player_input game_input.min, game_input.max, test_input: 11
  # call your assertions
end

是的,这需要一点代码重构,但从长远来看会更好。可能还有更好的重构方法,这样你就不会在本质上测试#loop或#gets in #player_input,因为你实际上是在测试#verify_input,它已经有了一个额外的可测试参数!

简化方法逻辑

您可以通过使用extract-method模式、将错误消息移动到验证器以及精简#player_input来大大简化代码。考虑这个替代方案,只要 @min@max 大于零,它就可以在代码方面工作(不一定适用于现有的测试)。

def ask_for_number
  print "Number: "
  gets.to_i
end

def verify_input min, max, input
  return true if input.between?(min, max)

  msg = "Error: enter number between #{min} and #{max}."
  p msg
end

def player_input min, max
  loop do
    number = ask_for_number
    break number if number.positive? && verify_input(min, max, number)
  end
end

这个逻辑在Ruby 3中很好用。2.2,简单的方法更容易测试,因为你基本上可以忽略#ask_for_number,因为你通常应该假设像#print和#这样的内置方法可以正常工作。如果他们不这样做,你有更大的问题。
这些较小的方法更容易测试和调试,也更容易推理。此外,使用Kernel#p而不是Kernel#puts(它总是返回nil)意味着你有一个更好的方法来测试你的输出作为一个返回值,而不是试图捕获输出。虽然调用$stderr.puts msg可能比调用p msg更正确,但使用#p可以防止您必须在STDERR上Assert某些内容,而不仅仅是查看返回的String。
您可能仍然需要在循环中做一些事情,例如添加额外的break或return语句,如果您希望在测试中接收特定值时提前退出。你的问题不是返回值,甚至不是循环本身;事实上,Ruby正在等待输入,当 not 正在测试时重新提示用户可能是正确的做法。毕竟,如果您只想在所有情况下运行一次,那么在循环中运行输入代码真的没有多大意义。

相关问题