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.