RSpec: simulating user input (via gets) without the test prompting for it - ruby

I'm coding a game by taking a TDD first approach, and have gotten stuck because the test keeps stopping for user input (repo is here).
I want the test to simulate user input rather than prompting for it, as I've set up some let keywords and have tried to account for user input that comes in via gets.chomp.
Here is where the game prompts for user input:
game.rb
module ConnectFour
class Game
def start_game
puts 'Welcome to Connect Four.'
puts "Enter name of player 1 (red)"
player1name = gets.chomp
player1 = Player.new(player1name)
end
end
end
And here is the test code:
game_spec.rb
require 'spec_helper'
module ConnectFour
describe Game do
let(:game) { Game.new }
let(:player1name) { 'Bob' }
let(:player1) { Player.new(player1name) }
describe 'Instantiate game play objects' do
describe 'Create player 1' do
it 'Provide player 1 name' do
allow_any_instance_of(Kernel)
.to receive(:gets)
.and_return(player1name)
end
it 'Instantiate player 1' do
expect(player1.name).to eq player1name
end
end
end # describe 'Instantiate game play objects'
end # Describe 'Game'
end
So far I've tried encapsulating the gets.chomp in its own method as recommended here but this has no effect. I've also tried prefixing $stdin to gets.chomp statements in the Ruby code but yeah, that was pretty useless. I had asked a similar question here recently and thought I had understood how to simulate user input but obviously not... any help would be appreciated.

use allow_any_instance_of(Object) instead of Kernel. The module Kernel is included into Object. Kernel is not ever actually instantiated because it's a module.
kind of a small point, but it'd be more accurate if you stubbed gets to return a strinng ending in \n, otherwise you could remove the chomp from the tested functionn and the test will still pass
reproducable example
require 'rspec'
require 'rspec/expectations'
test_case = RSpec.describe "" do
it "" do
allow_any_instance_of(Object).to receive(:gets).and_return "something\n"
puts gets.chomp
end
end
test_case.run

Related

Testing gets in rspec (user input)

My class has this #run method that so far is just this, to test the testing:
def run
puts "Enter 'class' to create a new class."
input = $stdin.gets.chomp
binding.pry
And in the tests so far I've got
allow($stdin).to receive(:gets).and_return 'class'
cli.run
Doing it this way I am able to see, in the pry session, that input has been set to 'class', as intended.
Is there a way to do with without adding $stdin to my call to gets in my method itself? i.e., input = gets.chomp
I've tried allow(cli.run).to receive(:gets).and_return 'class'
But then in the pry session, input is equal to the first line of the spec file!
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

Using RSpec to test user input with gets

I'm new to Unit Testing using RSpec and Ruby and I have a question on how to test if my code is using the gets method, but without prompting for user input.
Here is the code I'm trying to test. Nothing crazy here, just a simple one liner.
my_file.rb
My_name = gets
Here's my spec.
require 'stringio'
def capture_name
$stdin.gets.chomp
end
describe 'capture_name' do
before do
$stdin = StringIO.new("John Doe\n")
end
after do
$stdin = STDIN
end
it "should be 'John Doe'" do
expect(capture_name).to be == 'John Doe'
require_relative 'my_file.rb'
end
end
Now this spec works, but when I run the code it prompts for user input. I don't want it to do that. I want to simply test if the gets method is being called and possibly mock the user input. Not to sure how to achieve this in RSpec. In Python I would utilize unittest.mock is there a similar method in RSpec?
Thanks in advance!
Here's how you could stub gets with your return value.
require 'rspec'
RSpec.describe do
describe 'capture_name' do
it 'returns foo as input' do
allow($stdin).to receive(:gets).and_return('foo')
name = $stdin.gets
expect(name).to eq('food')
end
end
end
Failures:
1) should eq "food"
Failure/Error: expect(name).to eq('food')
expected: "food"
got: "foo"
(compared using ==)
To test if something is being called (such as a function) you can use expect($stdin).to receive(:gets).with('foo') to ensure it is being called (once) with the right args. The expectation line in this scenario has to go before name = $stdin.gets.

rspec 3 error when user is prompted for input

I have code that is requesting the user for input, such as:
class Foo
def prompt_for_foobar
puts "where is the foobar?"
gets.chomp
end
end
I would like to test that my application is asking "where is the foobar?". My test will pass upon commenting out 'gets.chomp'. but that is needed and the anything else I have tried has given me a Errno::ENOENT: error.
it "should prompt user" do
console = Foo.new
request = "where is the foobar?"
expect { console.prompt_for_foobar }.to output(request).to_stdout
end
What is the best way to test this method?
Not sure if this is the best way to handle this, but you can send puts and gets to STDOUT and STDIN.
class Foo
def prompt_for_foobar
STDOUT.puts "where is the foobar?"
STDIN.gets.chomp
end
end
Then, test that STDIN receives that puts message with your desired object.
describe Foo do
let(:foo) { Foo.new }
before(:each) do
allow(STDIN).to receive(:gets) { "user input" }
end
describe "#prompt_for_foobar" do
it "prompts the user" do
expect(STDOUT).to receive(:puts).with("where is the foobar?")
foo.prompt_for_foobar
end
it "returns input from the user" do
allow(STDOUT).to receive(:puts)
expect(foo.prompt_for_foobar).to eq "user input"
end
end
end
The problem is that gets is a method that forces human interaction (at least in the context of RSpec, where stdin isn't connected to a pipe from another process), but the entire point of automated testing tools like RSpec are to be able to run tests without involving human interaction.
So, rather than relying directly on gets in your method, I recommend you rely on a collaborator object that implements a particular interface -- that way in the test environment you can provide an implementation of that interface that provides a response without human interaction, and in other environments it can use gets to provide a response. The simplest collaborator interface here is probably a proc (they're perfect for this kind of thing!), so you could do the following:
class Foo
def prompt_for_foobar(&responder)
responder ||= lambda { gets }
puts "where is the foobar?"
responder.call.chomp
end
end
RSpec.describe Foo do
it 'prompts the user to respond' do
expect { Foo.new.prompt_for_foobar { "" } }.to output(/where is the foobar/).to_stdout
end
it "returns the responder's response" do
expect(Foo.new.prompt_for_foobar { "response" }).to eq("response")
end
end
Notice that prompt_for_foobar no longer calls gets directly; instead it delegates the responsibility of getting a response to a responder collaborator. By default, if no responder is provided, it uses gets as a default implementation of a responder. In your test you can easily provide a responder that requires no human interaction simply by passing a block that returns a string.

as_null_object not passing with two inputs

I'm working through the RSpec Book, and I have the following test code:
require 'spec_helper'
module Codebreaker
describe Game do
describe "#start" do
let(:output) { double('output').as_null_object }
let(:game) { Game.new(output) }
it "sends a welcome message" do
output.should_receive(:puts).with('Welcome to Codebreaker!')
game.start
end
it "prompts for the first guess" do
output.should_receive(:puts).with('Enter guess:')
game.start
end
end
end
end
which corresponds to the following code:
module Codebreaker
class Game
def initialize(output)
#output = output
end
def start
#output.puts 'Welcome to Codebreaker!'
#output.puts 'Enter a guess:'
end
end
end
Since I've set :output up as a double.as_null_object, I expect it to ignore any arguments/methods it is not expecting. For the first test (sends a welcome message), that's what it does, and it passes. The second test, however, is giving me this error:
Failure/Error: output.should_receive(:puts).with('Enter guess:')
Double "output" received :puts with unexpected arguments
expected: ("Enter guess:")
got: ("Welcome to Codebreaker!"), ("Enter a guess:")
# ./spec/codebreaker/game_spec.rb:16:in `block (3 levels) in <module:Codebreaker>'
Why is the double returning both "Welcome to Codebreaker!" and "Enter a guess" when I have explicitly told it to only expect "Enter a guess:", and how can I fix this while maintaining this same setup/structure?
The second case is failing because you have a typo in your expectation. You meant Enter a guess: instead of Enter guess:.
Unfortunately, rspec is very picky about wording on strings. In your start method you wrote "Enter guess" instead of "Enter a guess:".
It's important to follow the wording to a T, when you start having to raise an error, rspec gives you a very nasty response.
Good luck! Rspec is a great tool as you get further into it.

RSpec: how do I write a test that expects certain output but doesn't care about the method?

I'm trying to get my head around test-driven design, specifically RSpec. But I'm having trouble with some of the examples from The RSpec Book.
In the book, we test for output on $STDOUT like this:
output = double('output')
game = Game.new
output.should_receive(:puts).with('Welcome to Codebreaker!')
game.start()
Well, that works after a fashion. But why on earth should I care if the Game object uses the puts() method? If I change it to print(), should it really break the test? And, more importantly, isn't this against the one of the principals of TDD - that I should be testing what the method does (the design) rather than how it does it (the implementation)?
Is there some way I can write a test that just tests what ends up on $STDOUT, without looking at what method puts it there?
Create a display class with the ability to write the status out.
You production code will make use of this display object so you are free to change how you write to STDOUT. There will be just one place for this logic while your tests rely on the abstraction.
For example:
output = stub('output')
game = Game.new(output)
output.should_receive(:display).with('Welcome to Codebreaker!')
game.start()
While your production code will have something such as
class Output
def display(message)
# puts or whatever internally used here. You only need to change this here.
end
end
I'd make this test pass by doing the following:
def start
#output.display('Welcome to Codebreaker!')
end
Here the production code doesn't care how the output is displayed. It could be any form of display now the abstraction is in place.
All of the above theory is language agnostic, and works a treat. You still mock out things you don't own such as third party code, but you are still testing you are performing the job at hand via your abstraction.
take a look at this post. Nick raised questions about the same example, and a very interesting conversation follows in the comments. Hope you find it helpful.
Capture $stdout and test against that instead of trying to mock the various methods that might output to stdout. After all, you want to test stdout and not some convoluted method for mimicking it.
expect { some_code }.to match_stdout( 'some string' )
Which uses a custom Matcher (rspec 2)
RSpec::Matchers.define :match_stdout do |check|
#capture = nil
match do |block|
begin
stdout_saved = $stdout
$stdout = StringIO.new
block.call
ensure
#capture = $stdout
$stdout = stdout_saved
end
#capture.string.match check
end
failure_message_for_should do
"expected to #{description}"
end
failure_message_for_should_not do
"expected not to #{description}"
end
description do
"match [#{check}] on stdout [#{#capture.string}]"
end
end
RSpec 3 has changed the Matcher API slightly.
failure_message_for_should is now failure_message
failure_message_for_should_not is now failure_message_when_negated
supports_block_expectations? has been added to make errors clearer for blocks.
See Charles' answer for the complete rspec3 solution.
The way I'd test it is with a StringIO object. It acts like a file, but doesn't touch the filesystem. Apologies for the Test::Unit syntax - feel free to edit to RSpec syntax.
require "stringio"
output_file = StringIO.new
game = Game.new(output_file)
game.start
output_text = output_file.string
expected_text = "Welcome to Codebreaker!"
failure_message = "Doesn't include welcome message"
assert output_text.include?(expected_text), failure_message
I came across this blog post which helped me solve this issue:
Mocking standard output in rspec.
He sets up before/after blocks, and I ended up doing them inside the actual rspec itself, for some reason I couldn't get it to work from my spec_helper.rb as recommended.
Hope it helps!
An updated version of Matt's answer for RSpec 3.0:
RSpec::Matchers.define :match_stdout do |check|
#capture = nil
match do |block|
begin
stdout_saved = $stdout
$stdout = StringIO.new
block.call
ensure
#capture = $stdout
$stdout = stdout_saved
end
#capture.string.match check
end
failure_message do
"expected to #{description}"
end
failure_message_when_negated do
"expected not to #{description}"
end
description do
"match [#{check}] on stdout [#{#capture.string}]"
end
def supports_block_expectations?
true
end
end

Resources