When writing unit tests for my simplesem interpreter, one test in particular was problematic. In simplesem, the
set write instruction prints output to the screen.
// place Hello World! on the 'write' buffer set write, "Hello World!"
Internally, the interpreter is just passing the second parameter, “Hello World!”, to the
puts method in Ruby. This makes it difficult to use traditional test/unit assertions to check that the simplesem instruction is working.
require 'test/unit' require 'rubygems' require 'mocha' class SimpleSemParserTest < Test::Unit::TestCase def test_set_stmt_write parser = SimpleSemParser.new parser.expects(:puts).with("Hello World!") parser.parse('set write, "Hello World!"').execute end end
This solution is fine for most situations; Mocha will throw an exception if
puts is not called. However in my case, it was unsuitable because
puts was not being called on the SimpleSemParser object but instead on a Treetop syntax node that I did not easily have access to within the unit test.
I knew that if I could capture the output from the
puts method into a variable I would be able write the test using a standard
assert_equal. After some googling I discovered that this functionality is built into the ZenTest gem. After rewriting the test looked like this.
require 'test/zentest_assertions' class SimpleSemParserTest < Test::Unit::TestCase def test_set_stmt_write out, err = util_capture do parser = SimpleSemParser.new parser.parse('set write, "Hello World!"').execute end assert_equal "Hello World!\n", out.string end end
This works great. However, should we decided that we do not want to use an external gem, with a little effort we can bypass ZenTest and implement
require 'stringio' module Kernel def capture_stdout out = StringIO.new $stdout = out yield return out ensure $stdout = STDOUT end end
Done! We extended the Kernel module with a method called
capture_stdout works by redirecting
$stdout to an instance of StringIO. StringIO has all of the IO methods, but acts on a string instead of a file. After changing
$stdout we yield to let the caller generate output. Once the yield is finished, we return the StringIO instance and add an ensure to guarantee that
$stdout is reset to its default value. Adding
capture_stdout to the Kernel module has the effect of giving
capture_stdout a global scope so that it can be used anywhere.
With the exception that we that we renamed the method to
capture_stdout, and are not returning
$stderr, the unit test has not changed from the ZenTest version.
class SimpleSemParserTest < Test::Unit::TestCase def test_set_stmt_write out = capture_stdout do parser = SimpleSemParser.new parser.parse('set write, "Hello World!"').execute end assert_equal "Hello World!\n", out.string end end
We end with a clean, very readable way to test Ruby methods that generate output without using any libraries.
I prefer this name over “util_capture”. ↩