one assertion per testをツールで - dustを拡張してみる

ひとつのテストメソッドにはひとつのassert文、そしてEmacsサポート - http://rubikitch.com/に移転しましたにてone assertion per test推奨と言ったが、冗長すぎるのが少し嫌。

dustを使うと、

require 'rubygems'
require 'test/unit'
require 'dust'
unit_tests do
  test "Foo#initialize test (a)" do
    x = Foo.new(:a=>1, :b=>2)
    assert_equal 1, x.a
  end

  test "Foo#initialize test (b)" do
    x = Foo.new(:a=>1, :b=>2)
    assert_equal 2, x.b
  end
end

なんて書ける。内部で、

module UnitTest
  class Test1 < Test::Unit::TestCase
    def test_Foo_initialize_test__a_
      x = Foo.new(:a=>1, :b=>2)
      assert_equal 1, x.a
    end

    def test_Foo_initialize_test__b_
      x = Foo.new(:a=>1, :b=>2)
      assert_equal 2, x.b
    end
  end
end

に変換される。一方、こんな記法はどうだろう?

unit_tests do
  test "Foo#initialize test" do
    x = Foo.new(:a=>1, :b=>2)
    check("(a)") { assert_equal 1, x.a }
    check("(b)") { assert_equal 2, x.b }
  end
end

これも上のTest::Unitに変換されるようにする。Lispのマクロなら構文木いぢれるからこんなコードを生成できるけど、RubyだとParseTreeとか使わないとだめかも…それに1.9だと動かないかもしれんし。
one assertion per testするためにいちいちコピペしないといけないのは、実装上の都合があるようだ。

だけどこれならば、実現できるかもしれない。煩雑すぎるかな?

unit_tests do
  multi_test "Foo#initialize test" do
    before { Foo.new(:a=>1, :b=>2) }
    check("(a)") {|x| assert_equal 1, x.a }
    check("(b)") {|x| assert_equal 2, x.b }
    after { 後片付 } 
  end
end

実装

まぁとりあえずできた。提案として作者にメールしてみるか。

require 'rubygems'
require 'test/unit'
require 'active_support'        # for instance_exec
require 'dust'

class Foo
  def initialize(hash)
    @a = hash[:a]
    @b = hash[:b]
  end
  attr_reader :a, :b
end

class Test::Unit::TestCase
  class DustMultipleTest
    def before(&block)
      @before_proc = block
    end
    def after(&block)
      @after_proc = block
    end
    def check(name,&block)
      (@checks||=[]) << [name, block]
    end
    attr_reader :before_proc, :after_proc, :checks
  end
  
  def self.multi_test(basename, &block)
    dmt = DustMultipleTest.new
    dmt.instance_eval(&block)

    module_eval do
      dmt.checks.each do |name, assert_block|
        test("#{basename} #{name}") do
          begin
            x = instance_eval(&dmt.before_proc)
            instance_exec(x, &assert_block)
          ensure
            dmt.after_proc and instance_exec(x, &dmt.after_proc)
          end
        end
      end
    end
  end
end

unit_tests do
  multi_test "Foo#initialize test" do
    before { Foo.new(:a=>1, :b=>2) }
    check("(a)") {|x| assert_equal 1, x.a }
    check("(b)") {|x| assert_equal 2, x.b }
    after {|x| } 
  end
end