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:
- Unit tests should generally test method results, not replicate the internals.
- You're trying to use #allow without defining a double first.
- 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
- Test it by hand and rely that it won't break (recommended approach, TDD is not a religion).
- Get the subverted version of the shell in question and make it imitate the user, and compare
whetherget_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
How to Remove Duplicates in a Hash in Ruby on Rails
How to Parse a Url and Extract the Required Substring
How to Convert a Ruby String with Brackets to an Array
Getting a List of Classes That Include a Module
How to Stop Rails' Built-In Server from Listening on 0.0.0.0 by Default
Encryption-Decryption in Rails
Converting a Unique Seed String into a Random, Yet Deterministic, Float Value in Ruby
Access Current Git Commit Number from Within Heroku App
How to Store Nil User's Goal in a Session
Ruby - Calling Setters from Within an Object
Can You Install Documentation for Existing Gems
Rmagick - How to Find Out the Pixel Dimension of an Image
How to Inspect What Is the Default Value for Optional Parameter in Ruby's Method
Check to See If an Array Is Already Sorted
Rails Fastercsv "Unquoted Fields Do Not Allow \R or \N"
Paperclip Error: Model Missing Required Attr_Accessor for 'Avatar_File_Name'