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.