Тестирование STDIN в Ruby

В настоящее время я пытаюсь протестировать базовый метод, который получает некоторый ввод от пользователя (получает) и выводит его (puts). После небольшого исследования я нашел хороший способ протестировать стандартный выходной поток, который ниже:

def capture_standard_output(&block)
  original_stream = $stdout
  $stdout = mock = StringIO.new
  yield
  mock.string.chomp
ensure
  $stdout = original_stream
end

Метод, который я тестирую, приведен ниже, вывод и ввод относятся к ivars, которые я инициализирую в начале и указываю на эквивалентные $stdout и $stdin:

def ask_for_mark
  ouput.puts 'What shall I call you today?'
  answer = input.gets.chomp.capitalize
  answer
end

Теперь я видел некоторые решения для STDIN, но на самом деле не понял ни одного из них, и я определенно не хочу копировать и вставлять. Единственное, что я получил, чтобы "работать", - это тот, что приведен ниже, но он не работает с тех пор, как я запускаю rspec, он останавливается и ждет ввода и просто нажав enter, он передает:

  it "takes user name and returns it" do
    output = capture_standard_output { game.ask_for_name }
    expect(output).to eq "What shall I call you today?"
    game.input.stub(:gets) { 'joe' }
    expect(game.ask_for_name).to eq 'Joe'
  end

Что было бы хорошим способом тестирования STDIN? Я смотрел на экран большую часть дня (не очень хорошо, я знаю), поэтому свежий взгляд и некоторая помощь были бы весьма полезны: -)

Update

Для тех, кто сталкивается с подобными проблемами, я придерживался других (более простых) подходов. Во-первых, я бы отделил операции ввода-вывода в своих собственных методах.

В случае ввода в тестах он может быть предварительно заполнен нужными данными, поэтому при отправке сообщения gets он вернет те данные, которые разделены символом новой строки \n следующим образом:

input = StringIO.new("one\ntwo\n")
=> #<StringIO:0x007f88152f3510>
input.gets
=> "one\n"
input.gets
=> "two\n"
input.gets
=> nil

Это помогает сохранить внутренности тестируемого метода, приватно, не связывая тесты с деталями реализации.

Другой подход - просто использовать полиморфизм и передать объект Fake или Spy, который соответствует одному и тому же api, но вместо того, чтобы делать вызов stdin или stdout, он возвращает законченные данные вместо или в случае Spy, он регистрирует вызов.

class SpyIO
  def initialize
    was_called? = false
  end
  ...
  def ask_for_name
    // call to stdout would normally take place
    was_called? = true
  end
  ...
end

class FakeIO
  def initialize(data = [some, data])
    @data = data
  end

  def get_user_input
    // call to stdin would normally happen
    @data.shift
  end
  ...
end

Там есть компромиссы в каждом подходе, но я думал, что поставил бы их здесь, если бы у кого-то были подобные проблемы или варианты.

Ответ 1

Вы можете просто заглушить STDIN:

it "takes user name and returns it" do
  output = capture_standard_output { game.ask_for_name }
  expect(output).to eq "What shall I call you today?"
  allow(STDIN).to receive(:gets) { 'joe' }
  expect(game.ask_for_name).to eq 'Joe'
end

Собственно, вы можете сделать то же самое с STDOUT, не изменяя $stdout:

it "takes user name and returns it" do
  expect(STDOUT).to receive(:puts).with("What shall I call you today?")
  allow(STDIN).to receive(:gets) { 'joe' }
  expect(game.ask_for_name).to eq 'Joe'
end

Ответ 2

Ну, я не могу быть уверен, но одна проблема, которая у меня была (по-прежнему есть) при работе с $stdin/$stdout/StringIO, обязательно запускает #rewind на них. Вы это делаете?

input = StringIO.new
input.puts "joe"
input.rewind
input.gets #=> "joe\n"