Ruby MiniTest: запуск теста в бесконечном цикле

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

Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
0
0
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Почему вы должны делать тест в первую очередь в будущем

Вот почему вы должны сначала написать свои тесты. Гораздо проще тестировать код, который был разработан для тестирования, чем модифицировать код или тестовую среду постфактум. Учитесь на этом.

Самая простая вещь, которая может работать

Вместо того, чтобы исправлять свой код или свои тесты, просто заявите, что вы получаете исключение 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 сидит и ждет ввода, и повторный запрос пользователя, когда он не тестируется, вероятно, является правильным решением. На самом деле нет особого смысла запускать ваш входной код внутри цикла, если вы, в конце концов, хотите запустить его только один раз при любых обстоятельствах.

Другие вопросы по теме

Похожие вопросы

Соединение Google My Business API без перенаправления OAuth
Следует ли использовать оператор безопасной навигации для модульного тестирования с обоими сценариями (объект существует + объект ноль)?
Руби `.in?` означает?
Bundler не может разрешить версию для sorbet-static, соответствующую моей текущей платформе; Найден сорбет-статик... который не соответствует текущей платформе
Фильтр по вычисляемому значению в Active Record
Active-storage: Uncaught Ошибка: прямая загрузка не удалась: ошибка при создании большого двоичного объекта для «example.png». Статус: 500, ПОТЕНЦИАЛЬНАЯ ОШИБКА CORS?
Почему json.partial! строка в шаблоне jbuiler не выполняется при передаче локальных переменных с использованием нового синтаксиса хэша?
Правильно издеваетесь над переменными среды с помощью minitest?
Rails 7 — интеллектуальная область для многих
Ruby: при попытке установить драгоценные камни или обновить систему истекло время ожидания Gem::RemoteFetcher::FetchError)