# How to test Ruby methods that involve puts or gets

I recently saw a post on Reddit where the OP asked how to test a method which involved `puts` and `gets`. The example the OP posted looked like the following (which I’ve edited very slightly for clarity):

``````class Example
puts "Input an integer 5 or above"
loop do
input = gets.to_i
return true if input >= 5
puts "Invalid. Try again:"
end
end
end``````

What makes the `ask_for_number` method challenging to test is a dependency. Most methods can be tested by saying, “When I pass in argument X, I expect return value Y.” This one isn’t so straightforward though. This is more like “When the user sees output X and then enters value V, expect subsequent output O.”

Instead of accepting arguments, this method gets its value from user input. And instead of necessarily returning a value, this method sometimes simply outputs more text.

How can we give this method the values it needs, and how can we observe the way the method behaves when we give it these values?

## A solution using dependency injection (thanks to Myron Marston)

Originally, I had written my own solution to this problem, but then Myron Marson, co-author of Effective Testing with RSpec 3, supplied an answer of his own which was a lot better than mine. Here it is.

I comment on this solution some more below, but you can see in the `initialize` method that the `input`/`output` dependencies are being injected into the class. By default, we use `\$stdin`/`\$stdout`, and under test, we use something else for easier testability.

``````class Example
def initialize(input: \$stdin, output: \$stdout)
@input = input
@output = output
end

@output.puts "Input an integer 5 or above"
loop do
input = @input.gets.to_i
return true if input >= 5
@output.puts "Invalid. Try again:"
end
end
end

require 'stringio'

RSpec.describe Example do
context 'with input greater than 5' do
it 'asks for input only once' do

expect(output).to eq "Input an integer 5 or above\n"
end
end

context 'with input equal to 5' do
it 'asks for input only once' do

expect(output).to eq "Input an integer 5 or above\n"
end
end

context 'with input less than 5' do
it 'asks repeatedly, until a number 5 or greater is provided' do

expect(output).to eq <<~OUTPUT
Input an integer 5 or above
Invalid. Try again:
Invalid. Try again:
OUTPUT
end
end

input = StringIO.new(input_numbers.join("\n") + "\n")
output = StringIO.new

example = Example.new(input: input, output: output)

output.string
end
end``````

Under test, an instance of `StringIO` can be used instead of `\$stdout`, thus making the messages sent to `@output` visible and testable. That’s how we can “see inside” the `Example` class and test what at first glance appears to be a difficult-to-test piece of code.

## 5 thoughts on “How to test Ruby methods that involve puts or gets”

1. Myron Marston

This is what I’d do instead:

https://gist.github.com/myronmarston/d9a699c1c0c74b992ceb1bbe6b4b2c6c

We talk about this a fair bit in Effective Testing with RSpec 3, but any code that does I/O is going to be far better off being tested with StringIO instead of mocking the _exact method calls_ producing the output. `puts “a”; puts “b”` and `puts “a\nb”` behave _identically_, but you can’t write a test that mocks `puts` that would pass for both implementations, which is a problem. StringIO avoids this; it allows you to test what output was produced, without specifying _how_ the output was produced.

Going this route improves the design of the `Example` class, too:

* It clearly documents its dependencies in its initializer (previously they were implicit)
* Callers are able to control where the object gets its input from and writes its output to

1. Jason Swett Post author

Hey Myron, thanks for sharing this. I like your approach a lot better than mine. Would it be okay if I update my post to include your version (with full credit of course)?

2. Deen

Loved the article, Jason. I have a question that might be slightly outside this article’s scope, but it seems odd to me that an rspec `it` block testing for failure needs this passing test at the end to break the loop and return the test results to the CLI:
“`
1. Jason Swett Post author
{"ft_lang_label":"__label__en","ft_lang_prob":0.84675443,"math_prob":0.63349324,"size":4533,"snap":"2023-40-2023-50","text_gpt3_token_len":1117,"char_repetition_ratio":0.121218815,"word_repetition_ratio":0.07880435,"special_character_ratio":0.23538496,"punctuation_ratio":0.11363637,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.9652914,"pos_list":[0,1,2,3,4,5,6,7,8],"im_url_duplicate_count":[null,null,null,null,null,null,null,null,null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2023-12-11T22:48:17Z\",\"WARC-Record-ID\":\"<urn:uuid:98c9ff11-2b6e-4dda-964b-6550e0a248e7>\",\"Content-Length\":\"48272\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:8756c958-2f6d-4574-8525-38449e7fb432>\",\"WARC-Concurrent-To\":\"<urn:uuid:a8a78578-f027-4a02-b9e5-cf628a8a48d5>\",\"WARC-IP-Address\":\"104.198.110.160\",\"WARC-Target-URI\":\"https://www.codewithjason.com/test-ruby-methods-involve-puts-gets/\",\"WARC-Payload-Digest\":\"sha1:CI3LRI3WXV2UFSPII4TLGM7CKASEBO77\",\"WARC-Block-Digest\":\"sha1:R3FGATFRR2DN7VARNHMTCZXSJRAJJLIM\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2023/CC-MAIN-2023-50/CC-MAIN-2023-50_segments_1700679518883.99_warc_CC-MAIN-20231211210408-20231212000408-00571.warc.gz\"}"}