Using Synthesis with Test::Unit and Mocha
George Malamidis, January 13th, 2008Synthesis is a Ruby library that applies a Synthesized Testing strategy, aiming to reduce the number of large, slow and brittle functional tests.
Imagine an example project with the following contents:
Macintosh-4:synthesis_example gm$ ls -R Rakefile lib test ./lib: synthesis_example synthesis_example.rb ./lib/synthesis_example: data_brander.rb storage.rb ./test: data_brander_test.rb storage_test.rb
synthesis_example.rb:
$: << File.dirname(__FILE__) + '/' require "synthesis_example/data_brander" require "synthesis_example/storage"
data_brander.rb:
class DataBrander
BRAND = "METAL"
def initialize(storage)
@storage = storage
end
def save_branded(data)
@storage.save "#{BRAND} - #{data}"
end
end
storage.rb:
class Storage
def initialize(filename)
@filename = filename
end
def save(val)
File.open(@filename, 'w') {|f| f << val}
end
end
Rakefile:
require "rubygems" require "synthesis/task" task :default => 'synthesis:test' Synthesis::Task.new
data_brander_test.rb:
%w(test/unit rubygems mocha).each { |l| require l }
require File.dirname(__FILE__) + "/../lib/synthesis_example"
class DataBranderTest < Test::Unit::TestCase
def test_saves_branded_to_storage
storage = Storage.new 'whatever'
storage.expects(:save).with('METAL - rock')
DataBrander.new(storage).save_branded 'rock'
end
end
Running data_brander_test.rb produces:
Macintosh-4:synthesis_example gm$ ruby test/data_brander_test.rb Loaded suite test/data_brander_test Started . Finished in 0.000487 seconds. 1 tests, 1 assertions, 0 failures, 0 errors
Supposing we haven’t written any tests for Storage yet, the outcome of the synthesis:test task is:
Macintosh-4:synthesis_example gm$ rake synthesis:test (in /Users/gm/devel/ruby/whatever_code/synthesis_example) [Synthesis] Collecting expectations... Loaded suite /usr/bin/rake Started . Finished in 0.00063 seconds. 1 tests, 1 assertions, 0 failures, 0 errors [Synthesis] Verifying expectation invocations... Loaded suite /usr/bin/rake Started . Finished in 0.000575 seconds. 1 tests, 1 assertions, 0 failures, 0 errors [Synthesis] [Synthesis] Tested Expectations: [Synthesis] [Synthesis] Untested Expectations: [Synthesis] Storage.new.save [Synthesis] [Synthesis] Ignoring: [Synthesis] [Synthesis] FAILED.
Synthesis will make a first pass at running the project’s tests collecting all simulated object interaction expectations. Then, it will run the tests again, verifying that the concrete implementations of the simulated expectation members have been covered in the tests.
In this example, the Synthesis task fails, reporting that the concrete implementation of the save instance method of Storage has not been tested. Let’s fix that.
storage_test.rb:
require "test/unit"
require "fileutils"
require File.dirname(__FILE__) + "/../lib/synthesis_example"
class StorageTest < Test::Unit::TestCase
def test_saves_to_file
Storage.new('test.txt').save('rock')
assert_equal 'rock', File.read('test.txt')
ensure
FileUtils.rm_f('test.txt')
end
end
Running rake again:
Macintosh-4:synthesis_example gm$ rake (in /Users/gm/devel/ruby/whatever_code/synthesis_example) [Synthesis] Collecting expectations... Loaded suite /usr/bin/rake Started .. Finished in 0.002721 seconds. 2 tests, 2 assertions, 0 failures, 0 errors [Synthesis] Verifying expectation invocations... Loaded suite /usr/bin/rake Started .. Finished in 0.002342 seconds. 2 tests, 2 assertions, 0 failures, 0 errors [Synthesis] [Synthesis] SUCCESS.
Traditionally, and on a project of a more realistic size than the example one, we would have to perform some sort of functional testing around the integration of BrandingService and Storage. Synthesis aims to provide enough confidence in order to eliminate the need for tedious functional tests.

January 13th, 2008 at 12:26 am
Looks cool. I played with it briefly just to check something out…
If you change the signature of Storage#save to be Storage#save(filename, mode), and update storage_test.rb accordingly, then Synthesis passes, even though the code is now broken.
I realize you said this is a prototype so I don’t want to jump the gun
How do you plan on handling that sort of case?
Thanks for posting this though, it looks like a step in the right direction.
January 13th, 2008 at 12:42 am
[...] « Using Synthesis with Test::Unit and Mocha [...]
January 13th, 2008 at 1:21 am
Hey George,
No, data_brander_test.rb doesn’t break, which was my point.
I just changed storage.rb and storage_test.rb accordingly.
http://rafb.net/p/HBuzye66.html
So all the tests pass, even though the two components can’t be used together.
I’ve thought about stuff like this in the past. I have a tough time seeing how you’d make this work without using the real implementation.
January 13th, 2008 at 1:24 am
Pat,
Sorry, I deleted my initial response when I realized what the problem you were describing was, intending to reply with this…
I know what you mean, now.
This is a Mocha issue, rather than a Synthesis issue. As you probably know, Mocha mocks are ducks. There’s Mocha::Mock#responds_like, but it won’t solve the problem you’re describing.
In the long run, we might end up with a stricter Mocking framework, which, as you’re saying, will have to tap into the methods of the actual Objects being mocked, as opposed to ducking them.
Thanks for bringing this up.
January 13th, 2008 at 1:40 am
Oh totally. It isn’t a problem with Synthesis anymore than it is a problem with using mocks in general.
It just seems like at *some* level you have to have integration tests.
January 14th, 2008 at 8:18 am
Pat:
I agree that you do need integration tests at some level. However with a tool like this, could we rely on the business level acceptance tests only?
We (should) always have story level tests to prove that our behaviour is useful. There also often seems to be an accretion of functional tests who’s purpose seems to be to prove that the unit tests all join up. If we can prove this automagically, then we can do away with these tests?
January 14th, 2008 at 11:52 pm
As of version 0.0.2, Synthesis will fail for the discussed scenario.
January 27th, 2008 at 1:43 am
[...] Let’s revisit the example project from the Using Synthesis with Test::Unit and Mocha article and change the save method of the Storage class to take an additional argument - mode. [...]
August 18th, 2008 at 1:11 am
This is something I dearly wanted when I was playing with rspec a year ago. My specs would pass but my app would fail because i mocked out things that didn’t exist. (I didn’t experience the problem as much using test/unit + mocha because I found I didn’t mock out as much stuff, so less opportunity to forget to code behind the mock).
August 18th, 2008 at 12:50 pm
Hey Nic, Synthesis has come a long way since this post and supports both Mocha and RSpec now.