Using Synthesis with Test::Unit and Mocha

Synthesis 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.

10 Responses to “Using Synthesis with Test::Unit and Mocha”

  1. Pat Maddox Says:

    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.

  2. Nutrun &#187; Blog Archive &#187; Using Synthesis with Expectations Says:

    [...] &laquo; Using Synthesis with Test::Unit and Mocha [...]

  3. Pat Maddox Says:

    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.

  4. George Malamidis Says:

    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.

  5. Pat Maddox Says:

    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.

  6. Stuart Caborn Says:

    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?

  7. George Malamidis Says:

    As of version 0.0.2, Synthesis will fail for the discussed scenario.

  8. Nutrun &#187; Blog Archive &#187; Synthesis 0.0.2 Says:

    [...] Let&#8217;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. [...]

  9. Dr Nic Says:

    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).

  10. George Malamidis Says:

    Hey Nic, Synthesis has come a long way since this post and supports both Mocha and RSpec now.

Leave a Reply