Thursday, May 31, 2007

Testing: Replace Mock with Stub

I often hear the phrase: I remember when I thought Mocks were the silver bullet. In fact, I remember using Mocks like they were the silver bullet. Unfortunately, using Mocks excessively can lead to a test suite that is just as brittle, if not more brittle than using concrete classes as dependencies.

One of the big reasons I began using Mocks in tests was to avoid cascading failures. However, I quickly learned that cascading failures still occur when using mocks, just in another flavor. Any implementation change required each instance of the mock to be updated to reflect the new implementation. My "solution" was just as bad as my problem.

To resolve this issue I started relying on Stubs. In fact, I found that using both Mocks and Stubs together made my tests more expressive. When I wrote that entry I was using C# in my day job; however, I think the concept transcends languages.

I'm going to use an example similar to the one from the original entry, this time in Ruby. It's completely acceptable to write the test using only mocks. (Mocha used for mocking)

class SqlStatementBuilder
attr_reader :instance, :where
def initialize(instance, where)
@instance, @where = instance, where
end

def create_find_all_statement
"select * from #{instance.name.to_s} #{where.to_sql}"
end
end

class SqlStatementBuilderTests < Test::Unit::TestCase
def test_instance_name_is_used_as_table_name
instance = mock
instance.expects(:name).returns('Foo')
where = mock
where.expects(:to_sql).returns("")
builder = SqlStatementBuilder.new(instance, where)
assert_equal "select * from Foo ", builder.create_find_all_statement
end

def test_where_to_string_is_appended_to_end
instance = mock
instance.expects(:name).returns('')
where = mock
where.expects(:to_sql).returns("where ...")
builder = SqlStatementBuilder.new(instance, where)
assert_equal "select * from where ...", builder.create_find_all_statement
end
end

While using mocks does verify the behavior of the builder class, that verification is superfluous. In fact, I believe the above test is covering too much. The state based assertion (assert_equals ...) at the end is all that's really necessary. The mocks can be replaced with stubs since the state based assertion is sufficient.

class SqlStatementBuilderTests < Test::Unit::TestCase
def test_instance_name_is_used_as_table_name
builder = SqlStatementBuilder.new(stub(:name => 'Foo'), stub(:to_sql => ''))
assert_equal "select * from Foo ", builder.create_find_all_statement
end

def test_where_to_string_is_appended_to_end
builder = SqlStatementBuilder.new(stub(:name => ''), stub(:to_sql => 'where ...'))
assert_equal "select * from where ...", builder.create_find_all_statement
end
end

Looking at the above example your first instinct might be to combine the tests. An alternative and possibly superior solution is to continue to test the interactions independently, but only pass in stubs that are necessary for each individual interaction. This is where the stub_everything method from Mocha becomes very handy.

class SqlStatementBuilderTests < Test::Unit::TestCase
def test_instance_name_is_used_as_table_name
builder = SqlStatementBuilder.new(stub(:name => 'Foo'), stub_everything)
assert_equal "select * from Foo ", builder.create_find_all_statement
end

def test_where_to_string_is_appended_to_end
builder = SqlStatementBuilder.new(stub_everything, stub(:to_sql => 'where ...'))
assert_equal "select * from where ...", builder.create_find_all_statement
end
end

Now imagine that the SqlStatementBuilder needs to be changed to allow a prefix for the table name.

class SqlStatementBuilder
attr_reader :instance, :where
def initialize(instance, where)
@instance, @where = instance, where
end

def create_find_all_statement
"select * from #{instance.prefix unless instance.prefix.nil?}#{instance.name.to_s} #{where.to_sql}"
end
end

Using the tests from the last example only one test will fail. This is because the test_where_to_string_is_appended_to_end test method only cares about the interaction with the where stub. A quick fix has both our tests running again. Again, the quick fix was isolated to the first test.

class SqlStatementBuilderTests < Test::Unit::TestCase
def test_instance_name_is_used_as_table_name
builder = SqlStatementBuilder.new(stub(:name => 'Foo', :prefix => 't.'), stub_everything)
assert_equal "select * from t.Foo ", builder.create_find_all_statement
end

def test_where_to_string_is_appended_to_end
builder = SqlStatementBuilder.new(stub_everything, stub(:to_sql => 'where ...'))
assert_equal "select * from where ...", builder.create_find_all_statement
end
end

A closing quick tip: The stub_everything method also takes a hash.

def test_stub_everything
a_stub = stub_everything(:int => 1, :str => "string")
a_stub.int # => 1
a_stub.str # => "string"
a_stub.anything_else # => nil
end

Utilizing a stub_everything with a hash you can create stubs that return values you care about and nil for everything else. Used correctly this can also increase the robustness of your test suite.
Post a Comment