Congratulations. That's a pretty nice first attempt.
I see only a few issues:
The problem statement shows that you will receive one number per line; your code expects one line with multiple numbers, separated by commas.
The problem statement asks you to print the numbers; your program doesn't.
When run, the test writes its prompt to $stdout. Tests should be quiet when passing.
The issue with the prompt can be fixed with an expectation:
@life.should_receive(:puts)\
.with("List comma(',') separated numbers. Press enter when done"
If your intent is to solve a slightly different problem, then we won't worry about the differences between what your program does and what they're asking for. But read below the fold for a simple test and implementation, TDD Style, for the stated problem.
This problem can be solved pretty simply. For example,
loop do
n = gets.chomp.to_i
puts n
break if n == 42
end
What test or tests could you write that would allow you to have code this simple? Let's see if we can get there from nothing:
class Life
def process_input
end
end
describe Life do
it "should copy numbers from $stdin to $stdout" do
life = Life.new
life.should_receive(:gets).with(no_args).and_return('41')
life.should_receive(:puts).with(41)
life.process_input
end
end
Running that, we find that the spec fails because the code never called gets
. We add the call to gets
, then find that the spec fails because the code never called puts
. Adding them, we get a passing test:
class Life
def process_input
puts gets.to_i
end
end
Of course, we've got no loop, so let's tell the test there should be multiple calls to gets and puts. We'll use .ordered
to tell rspec that the calls should occur in a certain order.
it "should copy numbers from $stdin to $stdout" do
life = Life.new
life.should_receive(:gets).ordered.with(no_args).and_return('41')
life.should_receive(:puts).ordered.with(41)
life.should_receive(:gets).ordered.with(no_args).and_return('42')
life.should_receive(:puts).ordered.with(42)
life.process_input
end
Now there needs to be a loop. Let's add it:
def process_input
loop do
puts gets.to_i
end
end
Our test neither passes nor fails. Instead, it hangs. That's because we didn't give it any way to get out of the loop. Let's add the termination condition:
def process_input
loop do
n = gets.to_i
puts n
break if n == 42
end
end
So, we can write a test that lets us have pretty simple code. But in a few ways, this test is bothersome. One reason is that it is too picky about the way that the I/O is done. What if we changed the program to use print instead of puts? It would have produce exactly the same output, but the test would fail. Also, having the test hang when the program fails to terminate isn't very good. We want tests that pass or fail quickly and cleanly, with no guessing. So let's rewrite our test (and the program) using dependency injection for I/O.
Starting over, we're going to change the the Life program so that it takes and input object and an output object:
class Life
def initialize(input = $stdin, output = $stdout)
@input = input
@output = output
end
def process_input
end
end
And our test:
require 'stringio'
describe Life do
it "should copy numbers from input to output" do
input = StringIO.new("41\n")
output = StringIO.new
life = Life.new(input, output)
life.process_input
output.string.should == "41\n"
end
end
Using StringIO instances as mock I/O objects is very handy, and lets us test the result of the I/O rather than the actions that produce the I/O. That's nice.
Since our program doesn't process anything yet, the test fails:
1) Life should copy numbers from $stdin to $stdout
Failure/Error: output.string.should == "41\n"
expected: "41\n"
got: "" (using ==)
Let's fix that:
def process_input
@output.print @input.gets
end
The test now passes. Let's modify the test to show that it should loop:
it "should copy numbers from $stdin to $stdout" do
input = StringIO.new("41\n42\n")
output = StringIO.new
life = Life.new(input, output)
life.process_input
output.string.should == "41\n42\n"
end
The test fails:
1) Life should copy numbers from $stdin to $stdout
Failure/Error: output.string.should == "41\n42\n"
expected: "41\n42\n"
got: "41\n" (using ==)
Let's add the loop:
def process_input
loop do
@output.print @input.gets
end
end
Now it hangs again! That's because I/O objects like StringIO and $stdin just return a nil when they run out of input. Our program is happily printing nil over and over. Let's make the test die nicely when the loop fails to terminate:
it "should copy numbers from $stdin to $stdout" do
input = StringIO.new("41\n42\n43\n")
def input.gets
super.tap { |s| raise "no more input" unless s }
end
output = StringIO.new
life = Life.new(input, output)
life.process_input
output.string.should == "41\n42\n"
end
Here we monkey patch our input objects so that when it runs out of input, it raises an error rather than just returning nil. The result:
1) Life should copy numbers from $stdin to $stdout
Failure/Error: super.tap { |s| raise "no more input" unless s }
RuntimeError:
no more input
Great! let's get the loop to terminate when it encounters that "42":
def process_input
loop do
s = @input.gets
@output.print s
break if s == "42\n"
end
end
Now we've got a robust test that doesn't care about the details of how I/O is done, and code that is pretty simple.
There's just one more enhancement I'd make to this program, and that is to have it exit when it runs out of input. While not strictly required by the problem statement, a program that prints blank lines endlessly upon EOF is obnoxious! Now we need another "it" in our test:
it "should exit on EOF" do
input = StringIO.new("41\n<EOF>")
def input.gets
s = super
case s
when "<EOF>"
nil
when nil
raise "no more input"
else
s
end
end
output = StringIO.new
life = Life.new(input, output)
life.process_input
output.string.should == "41\n"
end
Our monkey-patched input object just got a little more interesting. We want it to return "nil" on end of input, because that's how the loop will detect end of input in real life, but we want it to raise an exception if the code under test tries to get more input after the nil. This test fails:
1) Life should exit on EOF
Failure/Error: raise "no more input"
RuntimeError:
no more input
So let's make the loop exit on end of input:
def process_input
loop do
s = @input.gets
@output.print s
break if s.nil?
break if s == "42\n"
end
end
Now the program meets the stated requirements, and our own requirement of not filling the output with blank lines on end-of-input.