UnitTestライブラリ

劣化expectations+a。


ユースケース
descはRakeのそれみたいなもんです。
expect_withは検査用メソッド名を渡して呼ぶ。expectは#===で検査する。
expect_eql?みたいなのはmethod_missingでexpect_withに:eql?を渡した
感じに変換される。

自分で使いそうなのは後のふたつだろうか。expect_withはmethod_missing頼りだと演算子なメソッドで検査させられないと気づいてつけたもんだし。


2008/06/09修正: トレース情報の扱いを間違えてて行番号とかがわけわからん
ことになってました。修正。

unit_test do
  
  desc "Should raise for #hoge"
  expect NoMethodError do
    Object.hoge
  end
  
  expect SyntaxError do
    Object.piyo
  end
  
  desc "Should match"
  expect_with :===, /\AA.*?Z\Z/ do
    "AtoZ"
  end
  
  desc "Should not match because of `eql?'"
  expect_eql? /\AA.*?Z\Z/ do
    "AtoZ"
  end
  
  expect_eql? "AtoZ" do
    "AtoZ"
  end
  
end


↑の出力結果。

3/5:60.0%:oxoxo

C:/home/ruby/lib/unittest.rb:140:===
Expected : SyntaxError
Actual   : #<NoMethodError: undefined method `piyo' for Object:Class>

@ Should not match because of `eql?'
C:/home/ruby/lib/unittest.rb:150:eql?
Expected : /\AA.*?Z\Z/
Actual   : "AtoZ"

unittest.rb

ライブラリ本体。

require 'singleton'

module UnitTest
  
  class Suite
    
    include Singleton
    
    def initialize(*args, &block)
      super
      @last_description = nil
    end
    
    def cases
      @cases ||= []
    end
    
    def desc(*args)
      @last_description = args.join("\n")
    end
    
    def expect_with(comp, expected, &actual)
      poping_description do |desc|
        location = caller(2).find{|b|
          /:in `(?:method_missing|(expect(?:_.*?)?))'\Z/ !~ b
        }
        cases << Case.new(location, expected, actual, desc, comp)
      end
    end
    
    def expect(expected, &actual)
      expect_with :===, expected, &actual
    end
    
    def method_missing(name, *args, &actual)
      super unless /\Aexpect_/ =~ name.to_s
      expect_with $'.intern, *args, &actual
    end
    
    # 
    # Format:
    #   Successes/Total:Achivement[%]:StatusSequence
    #   
    #   @ Description [&optional]
    #   FileName:Caller:Comparer
    #   Expected : value
    #   Actual   : value
    # 
    def run
      return unless @cases
      results = cases.map{|c| c.evaluate }
      total   = results.size
      ns, nf  = results.partition{|r| r }.map{|x| x.size }
      ox = results.map{|r| r ? "o" : "x" }.join
      puts "#{ns}/#{total}:#{Float(ns) / total * 100}%:#{ox}\n"
      puts
      unless nf.zero?
        failures = cases.reject{|c| c.evaluate }
        failures.each do |failure|
          puts render_failure_case(failure)
          puts
        end
      end
    end
    
    private
    
    def poping_description
      yield @last_description
      @last_description = nil
    end
    
    def render_failure_case(f)
      buf = f.description ? "@ #{f.description}\n" : ""
      location, method = f.location.split(/:in\s*/, 2)
      method = method ? "#{method[1...-1]}():" : ''
      buf << "#{location}:#{method}#{f.comp}\n"
      buf << "Expected : #{f.expected.inspect}\n"
      buf << "Actual   : #{f.actual.inspect}\n"
      buf
    end
    
  end #class Suite
  
  class Case
    
    def initialize(location, expected, actual, desc = nil, comp = :===)
      @location    = location
      @expected    = expected
      @actual      = actual
      @description = desc
      @comp        = comp
    end
    
    attr_reader :location
    attr_reader :expected
    attr_reader :description
    attr_reader :comp
    
    def actual
      @actual_evaluated_cache ||= evaluate_actual()
    end
    
    def evaluate
      self.expected.send @comp, self.actual
    end
    
    private
    
    def evaluate_actual
      begin
        @actual.call
      rescue => ex
        ex
      end
    end
    
  end #class Case
  
end #module UnitTest



def unit_test(&block)
  UnitTest::Suite.instance.instance_eval(&block)
end

at_exit{ UnitTest::Suite.instance.run }


ユニットテストのなんたるかもきちんとわかってないうちからやっちったよ。
これ使えるんのか? 実用に足りてもたぶん自分はexpectations使うよな。

当日記は車輪の最発明を地で往く所存です。