Я хотел бы протестировать следующий код с помощью 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
Код минитеста: Код теста заглушает метод :gets с недопустимым вводом. А затем утверждает, что вывод метода player_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, чтобы убедиться, что код возвращает стандартную ошибку, которую можно подтвердить.

Вот почему вы должны сначала написать свои тесты. Гораздо проще тестировать код, который был разработан для тестирования, чем модифицировать код или тестовую среду постфактум. Учитесь на этом.
Вместо того, чтобы исправлять свой код или свои тесты, просто заявите, что вы получаете исключение Timeout::Error из модуля Timeout стандартной библиотеки, если по какой-либо причине вы застряли в своем цикле. Например:
require 'timeout'
assert_raises(Timeout::Error) do
Timeout::timeout(0.1) { @game_input.player_input(min, max) }
end
При условии, что это не проверяет конкретное сообщение об ошибке, оно решает реальную проблему, с которой вы сталкиваетесь в своем цикле, не требуя изменения существующей логики. Идите по этому пути, если вы просто хотите перестать зацикливаться на этом конкретном тесте и не заботиться о том, почему.
Предполагая, что вы хотите исправить свой код, а не просто избежать застревания в своем цикле, вы должны попытаться упростить свой код. Просто кажется плохой идеей заглушать Kernel#gets , даже если вы делаете это внутри блока. 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', если ваша тестовая среда не делает этого за вас. Вероятно, вам следует сделать так, чтобы 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 в #player_input, поскольку вы действительно пытаетесь протестировать #verify_input, который уже требует дополнительного проверяемого аргумента!
Вы можете значительно упростить свой код, используя шаблон метода извлечения, переместив сообщение об ошибке в валидатор и сократив #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 и #gets, просто работают. Если они этого не сделают, у вас есть большие проблемы.
Эти более мелкие методы легче тестировать и отлаживать, и о них намного проще рассуждать. Кроме того, использование Kernel#p вместо Kernel#puts (которое всегда возвращает nil) означает, что у вас есть лучший способ проверить свой вывод как возвращаемое значение, а не пытаться захватить вывод. Хотя, вероятно, правильнее вызывать $stderr.puts msg, чем p msg, использование #p избавляет вас от необходимости утверждать что-то в STDERR, а не просто искать возвращаемую строку.
Вам все еще может понадобиться сделать что-то внутри вашего цикла, например добавить дополнительный оператор break или return, если вы хотите выйти раньше при получении определенного значения во время тестирования. Ваша проблема на самом деле не в возвращаемом значении или даже в цикле как таковом; дело в том, что Ruby сидит и ждет ввода, и повторный запрос пользователя, когда он не тестируется, вероятно, является правильным решением. На самом деле нет особого смысла запускать ваш входной код внутри цикла, если вы, в конце концов, хотите запустить его только один раз при любых обстоятельствах.