Seam: A place where you can alter behavior in your program without editing in that place.
I came across some Ruby code I'd written in October 2006 that surprised me. I recognized it immediately for what it was--Java written in Ruby. Let me explain.
I wanted to write a test for a method Graph#write_graph:
class Graph def write_graph(file) formatter = Formatter.new formatter.write_header(file) @connections.each do |connection| formatter.write_connection(file, connection) end formatter.write_footer(file) end # ... end
I wanted to write a test like this:
class TestGraph < Test::Unit::TestCase context 'Graph' do should 'write a graph with a single connection' do graph = Graph.new graph.add_connection(Connection.new('1', '2')) file = '' graph.write_graph(file) assert_equal "1->2;\n", file end end end
The problem is that the assertion fails, since the actual graph contains a lengthy header, as you can see from the implementation of the formatter class:
class Formatter def write_header(file) file << "digraph G\n" file << "{\n" file << " graph[fontsize=8,concentrate=true];\n" file << " node[fontname=Helvetica];\n" file << " node[fontsize=8];\n" file << " node[height=.1];\n" file << " node[width=.1];\n" file << " edge[arrowsize=.25];\n" file << " ranksep=.2;\n" file << " nodesep=.08;\n" end def write_footer(file) file << '}' end def write_connection(file, connection) file << connection.source << '->' << connection.destination << ";\n" end end
I wanted to override Formatter#write_header and Formatter#writer_footer in the test so they became no-ops. How to do this? I settled on constructor injection--my tried and true way of inserting mocks and stubs in Java.
I added a parameterized initializer (the Ruby name for 'constructor') that takes a formatter object:
class Graph def initialize(graph_formatter) @graph_formatter = graph_formatter end def write_graph(file) @graph_formatter.write_header(file) @connections.each do |connection| @graph_formatter.write_connection(file, connection) end @graph_formatter.write_footer(file) end # ... end
This forced me to change the calling code to specify the formatter. This
class GraphFactory attr_reader :graph def initialize(parser) @parser = parser @graph = Graph.new end end
became this
class GraphFactory attr_reader :graph def initialize(parser) @parser = parser @graph = Graph.new(Formatter.new) end end
I then created a formatter subclass that stubbed out methods 'write_header' and 'write_footer':
class MockFormatter < Formatter def write_header(file) end def write_footer(file) end end
Lastly, I modified the test to use the mock formatter class:
class TestGraph < Test::Unit::TestCase context 'Graph' do should 'write a graph with a single connection' do graph = Graph.new(MockFormatter.new) graph.add_connection(Connection.new('1', '2')) file = '' graph.write_graph(file) assert_equal "1->2;\n", file end end end
That was where the code stood from October 2006 until I looked at it again recently. Constructor injection, setter injection, and using extract method to make creation methods overridable in tests are all vital maneuvers in the world of Java, but they are unneeded overhead in Ruby. The difference between Java and Ruby is that Java classes are hard (thanks to Jim Weirich for pointing this out to me). Java classes are not objects, are principally source code constructs, and are unchangeable at runtime. This is not the case in Ruby. I will show you a couple ways to skin this cat in Ruby.
First, let's change the code back to the way it was before we introduced constructor injection:
class Graph def write_graph(file) formatter = Formatter.new formatter.write_header(file) @connections.each do |connection| formatter.write_connection(file, connection) end formatter.write_footer(file) end # ... end
It turns out we don't need to change a line of this code to stub out Formatter#write_header and Formatter#write_footer.
Ruby classes are open. The first solution involves redefining the methods for the class in the test:
class Formatter def write_header(file) end def write_footer(file) end end class TestGraph < Test::Unit::TestCase context 'Graph' do should 'write a graph with a single connection' do graph = Graph.new graph.add_connection(Connection.new('1', '2')) file = '' graph.write_graph(file) assert_equal "1->2;\n", file end end end
The second (and my preferred) way is to use a test mock library that takes advantage of Ruby class interception. This example uses the Mocha test mock library, but all the popular test mock libraries support this functionality.
class TestGraph < Test::Unit::TestCase context 'Graph' do should 'write a graph with a single connection' do Formatter.any_instance.stubs(:write_header) Formatter.any_instance.stubs(:write_footer) graph = Graph.new graph.add_connection(Connection.new('1', '2')) file = '' graph.write_graph(file) assert_equal "1->2;\n", file end end end