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.
I eventually found two solutions for this. The first, suggested by David Stevenson at Pivotal Labs, is to use mocha to check that puts was called on the object.
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 util_capture ourselves.
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_stdout1. 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”. ↩
Thanks! This has really helped me out. I really hate saying things like “I can’t test this.”
Very helpful, thanks! I’m testing a debug function that only “puts” when in a Rack application is in development mode.
Thank you! Your elegant capture_stdout method was exactly what I needed for testing the “trace” feature of a little embedded interpreter I’m working on.
Thanks! This is insanely useful!
This is used in Thor.