How to Write an Rspec Test for a Ruby Method That Contains "Gets.Chomp"

How do I write an RSpec test for a Ruby method that contains gets.chomp?

You can stub out standard input stream like this:

require 'stringio'

def capture_name
$stdin.gets.chomp
end

describe 'capture_name' do
before do
$stdin = StringIO.new("James T. Kirk\n")
end

after do
$stdin = STDIN
end

it "should be 'James T. Kirk'" do
expect(capture_name).to be == 'James T. Kirk'
end
end

How do I write an Rspec test for a Ruby method that contains variable that contains 'gets.chomp'?

Use Code Injection Instead of Mocks or Stubs

You have multiple problems with your approach. I won't enumerate them all, but instead focus on three key mistakes:

  1. Unit tests should generally test method results, not replicate the internals.
  2. You're trying to use #allow without defining a double first.
  3. You seem to be trying to set a message expectation, instead of using a stub to return a value.

There are certainly other problems with your code and your tests, but that's where I'd start once you remove your reliance on #gets from within your test cases. For example, to test the various paths in your method, you should probably configure a series of tests for each expected value, where #and_return explicitly returns new, load, or whatever.

More pragmatically, you're most likely struggling because you wrote the code first, and now are trying to retrofit tests. While you could probably monkey-patch things to make it testable post-facto, you're probably better off refactoring your code to allow direct injection within your tests. For example:

def show_prompt
print prompt =<<~"EOF"

Welcome to chess! What would you like to do?

* Start a new Game -> Enter "new"
* Load a saved Game -> Enter "load"
* Exit -> Enter "exit"

Selection:\s
EOF
end

def introduction input=nil
show_prompt

# Use an injected input value, if present.
input ||= gets.chomp.downcase

case input
when "new" then instructions
when "load" then load_game
when "exit" then exit!
else introduction
end
end

This avoids the need to stub or mock an object in the first place. Your tests can now simply call #introduction with or without an explicit value. That allows you to spend your time testing your logic branches and method outputs, rather than writing a lot of scaffolding to support mocking of your IO#gets call or avoiding nil-related exceptions.

How do I test a function with gets.chomp in it?

You first separate the 2 concerns of the method:

def get_action
gets.chomp
end

def welcome_user
puts "Welcome to Jamaica and have a nice day!"
action = get_action
return "Required action was #{action}."
end

And then you test the second one separately.

require 'minitest/spec'
require 'minitest/autorun'

describe "Welcoming users" do
before do
def get_action; "test string" end
end

it "should work" do
welcome_user.must_equal "Required action was test string."
end
end

As for the first one, you can

  1. Test it by hand and rely that it won't break (recommended approach, TDD is not a religion).
  2. Get the subverted version of the shell in question and make it imitate the user, and compare
    whether get_action indeed gets what the user types.

While this is a practical answer to your problem, I do not know how to do 2., I only know how to imitate the user behind the browser (watir-webdriver) and not behind the shell session.

Testing gets in rspec (user input)

You can avoid this as such:

def run
puts "Enter 'class' to create a new class."
input = gets.chomp
end

describe 'gets' do
it 'belongs to Kernel' do
allow_any_instance_of(Kernel).to receive(:gets).and_return('class')
expect(run).to eq('class')
end
end

The method gets actually belongs to the Kernel module. (method(:gets).owner == Kernel). Since Kernel is included in Object and almost all ruby objects inherit from Object this will work.

Now if run is an instance method scoped in a Class I would recommend scoping the stubbing a bit more such that:

class Test
def run
puts "Enter 'class' to create a new class."
input = gets.chomp
end
end

describe 'gets' do
it 'can be stubbed lower than that' do
allow_any_instance_of(Test).to receive(:gets).and_return('class')
expect(Test.new.run).to eq('class')
end
# or even
it 'or even lower than that' do
cli = Test.new
allow(cli).to receive(:gets).and_return('class')
expect(cli.run).to eq('class')
end
end

Example

rspec issue believed to be with gets.chomp call

The reason is that gets reads from ARGV (argument variables), such as the -fd flag you're giving to rpsec.

You can recreate this situation pretty easily:

> ruby -e "puts gets.chomp" ASD
-e:1:in `gets': No such file or directory @ rb_sysopen - ASD (Errno::ENOENT)
from -e:1:in `gets'
from -e:1:in `<main>'

You can prevent this from happening by clearing out ARGV before calling gets.

> ruby -e "ARGV.clear; puts gets.chomp" ASD
asd # <-- I then type this
asd # <-- and this is printed

You can't just say ARGV = [] because it's a constant, but calling clear is fine because it's not redefining the variable.

In short, just put this somewhere before gets.chomp:

ARGV.clear

Here's another question on the topic: Ruby readline fails if process started with arguments

NoMethodError: undefined method `chomp' for nil:NilClass while running Ruby Rspec on gets.chomp

You're executing code in your class file.

new_game = ConnectFour.new
new_game.play_game

This will run every time you load the file, like when you're testing it. It will prompt for input and run gets. What it's getting is the code of the test file (for some rspec reason). That's invalid, so it keeps running gets until eventually there is no more input and gets returns nil.

Remove that from your class file. Code like that should be in a separate file which requires the class.



Related Topics



Leave a reply



Submit