# A 4KB test suite based on method names. # # MicroTest has two assertions: should and should_not. They start the method # name, after which you can put anything. A 'should' method expects true and # a 'should_not' method expects false to be returned. # # MicroTest includes MicroStub, a microscopic stubber. For example: # Verb.stub(:meth, true) => Verb.meth == true # Verb.stub(:meth) { |arg1,arg2| return_value } # # Example MicroTest: # # class BinaryMicroTest < MicroTest # def should_exit_cleanly # `ruby #{MYEXEC}`; $? == 0 # end # end # # Microtest also includes a built-in self-test: # # SELFTEST=true ruby microtest.rb # class MicroTest class MicroFailure < StandardError; end class << self def run init and report { execute } end def init @passed, @failed, @executed, @failures = 0, 0, 0, [] @planned, @plan = micros.inject(0) { |c, (k,v)| c += v.size; c }, micros end def fp(s); print s; STDOUT.flush; end def r(s); "\e[1;31m#{s}\e[0m"; end def g(s); "\e[1;32m#{s}\e[0m"; end def y(s); "\e[1;33m#{s}\e[0m"; end def execute puts "Running #{@planned} test(s)..." @plan.each do |k, units| i = k.new units.each do |u| i.setup expected = (u.to_s =~ /^should_not/ ? false : true) r = catch_failures(k, u) { expect(expected) { i.send(u) } } r ? (@passed += 1; print g(".")) : (@failed += 1; print r("F")) @executed += 1 i.teardown MicroStub.clear! end end end def expect(exp) raise MicroFailure, "should be #{exp}" unless yield == exp end def catch_failures(k, u) begin yield; true rescue Exception => e @failures << [k, u, e]; false end end def report a = Time.new; yield; z = Time.new puts "\n%0.1f microseconds.\n%d planned, %d executed, %d passed, %d failed.\n" % [(z - a) * 10**6, @planned, @executed, @passed, @failed] unless @failures.empty? puts r("\nFAILURE REPORT\n") @failures.each { |f| report_failure(f) } end end def report_failure(f) puts y("#{f[0]}##{f[1]}") + "\n\t#{f[2].class}\n\t#{f[2].message})" puts ("\t" << f[2].backtrace.join("\n\t")) end def micros @micros ||= {} end def method_added(symbol) (MicroTest.micros[self] ||= []) << symbol if symbol.to_s =~ /^should_/ end end def setup; end def teardown; end def difference(v); a = v.dup; yield; v != a; end end module MicroStub class StubError < StandardError; end class << self def stubs @stubs ||= [] end def save(obj, sym) obj.eigen.send(:alias_method, "_#{sym}_", sym) && stubs << [obj, sym] end def clear! stubs.each { |(obj,sym)| obj.eigen.send(:alias_method, sym, "_#{sym}_") } && stubs = [] end end def stub(symbol, returns = true, &block) raise StubError, "#{self} does not respond to #{symbol}" if !respond_to?(symbol) MicroStub.save self, symbol definition = (block_given? ? block : Proc.new{returns}) self.eigen.send :define_method, symbol, &definition self end end class Object include MicroStub def eigen; class << self; self; end; end end if ENV["SELFTEST"] == "true" class Verb; def self.i; "I"; end; end class MicroSelfTest < MicroTest def should_expects_true true end def should_not_expects_false false end def should_stub_with_argument Verb.stub(:i, "J").i == "J" end def should_stub_with_block Verb.stub(:i) { "K" }.i == "K" end def should_stub_with_arity Verb.stub(:i) { |a,b,c| "L" }.method(:i).arity == 3 end def should_isolate_stubs Verb.i == "I" end end end if ENV["BENCH"] == "true" class BenchTest < MicroTest for i in 1..2500; define_method("should_#{i}") { true }; end end end at_exit do MicroTest.run end