Intro
RSpec is powerful and vast, but after using it extensively, I came to realize that really good tests only use a small sliver of its feature set. I looked at alternatives like minitest and Bacon, but then I had an interesting thought: you could write awesome tests using just 2 methods:
- assert
- stub
TMF is an attempt to provide a minimal but useful testing tool for ruby. It's not even a gem, just copy the code and you're done. It's about 30 LOC at the moment.
There are no automated tests for TMF itself. My goal is to keep it as simple as possible. Using another testing tool to test TMF seems wrong, and using TMF to test itself is also not right. Since I strive to keep the code simple enough that its correctness can be verified by hand, for now I test TMF manually (with rigor!) using the examples in the README.
I will use TMF in my projects and discover if such a minimalistic tool can be practical. Along the way, I will refine its features and look forward to hearing what you think about TMF!
Usage:
require 'tmf' include TMF assert(1 + 1, :== => 2) # => true assert(1 + 1, :== => 3) # => TMF::ExpectationNotMet: Expected 2 == 3 assert(1, :> => 0) # true assert(1, :eql? => 1.0) # TMF::ExpectationNotMet: Expected 1 eql? 1.0 assert(1, :< => 2, :>= => 1, :is_a? => Fixnum ) # => true assert(Object.foo, :== => :bar) # => NoMethodError: undefined method `foo' for Object:Class stub(Object, :method => :foo) do # within this block, Object.foo returns nil assert Object.foo, :== => nil end # => true stub(Object, :method => :foo, :return => :bar) do # within this block, Object.foo returns :bar assert(Object.foo, :== => :bar) end # => true # Object.foo is no longer defined Object.foo # => NoMethodError: undefined method `foo' for Object:Class stub(Object, :method => :bar, :error => RuntimeError) do # within this block, Object.bar raises a RuntimeError Object.bar end # => RuntimeError: RuntimeError stub(Object, :method => :bar, :error => ZeroDivisionError.new('any message you want')) do # within this block, Object.bar raises a ZeroDivisionError Object.bar end # => ZeroDivisionError: any message you want Object.methods.grep /foo/ # => [] stub(Object, :method => :foo){ Object.methods.grep /foo/ } # => [:foo] Object.methods.grep /foo/ # => [] # stub can also override existing methods Object.to_s # => "Object" stub(Object, :method => :to_s, :return => :cheezburger) do Object.to_s end # => :cheezburger # outside the stub block, Object.to_s is back to normal Object.to_s # => "Object" # set a spy to check if the stub was called stub(Object, :spy => :foo) do 'y u no call ?' end # => TMF::ExpectationNotMet: Expected Object to receive foo stub(Object, :spy => :foo) do Object.foo end # => nil
A more detailed example
Let's say you have a file PROJECT_ROOT/lib/foo.rb with the following:
class Foo def bar :bar end end
And you also have a file PROJECT_ROOT/test/foo_test.rb with the following:
require_relative '../lib/foo.rb' require_relative './tmf.rb' include TMF f = Foo.new # passing test assert(f.class, :== => Foo) # => true # failing test assert(f.class, :== => 'Bar') # => TMF::ExpectationNotMet: Expected Foo == Bar # stub with passing test stub(f, :method => :class, :return => 'Bar') do assert(f.class, :== => 'Bar') end # => true # stub with failing test stub(f, :method => :bar, :return => :baz) do assert(f.bar, :== => :snafu) end # TMF::ExpectationNotMet: Expected baz == snafu # testing a raised error begin f.nothingthere rescue NoMethodError assert( $!.message.include?("undefined method `nothingthere'"), :== => true ) end # => true # stub to raise an error stub(f, :method => :bar, :error => ZeroDivisionError) do begin f.bar rescue assert $!, :is_a? => ZeroDivisionError end end # => true # stub to raise an error stub(f, :method => :bar, :error => ZeroDivisionError) do begin f.bar rescue assert $!, :is_a? => RuntimeError end end # => TMF::ExpectationNotMet: Expected ZeroDivisionError is_a? RuntimeError # stub with spy and return value stub(f, :spy => :bar, :return => :baz) do 'all your base are belong to us' end # => TMF::ExpectationNotMet: Expected #<Foo:0x007f85331b4ee0> to receive bar # stub with spy and return value stub(f, :spy => :bar, :return => :baz) do assert(f.bar, :== => :baz) end # => true # Multiple stubs via nesting stub(f, :method => :foo, :return => :bar) do stub(f, :method => :sna, :return => :fu) do assert( [f.foo, f.sna], :== => [:bar, :fu] ) end end # => true # Override previous stubs stub(f, :method => :foo, :return => :bar) do assert(f.foo, :== => :bar) stub(f, :method => :foo, :return => :baz) do assert(f.foo, :== => :baz) stub(f, :method => :foo, :return => :snafu) do assert(f.foo, :== => :snafu) end end end # => true # Chained stubs # e.g. f.foo.bar stub(f, :method => :foo) do stub( f.foo, :method => :bar, :return => :baz ) do assert(f.foo.bar, :== => :baz) end end # => true
Then, you can run the tests above with:
$ ruby test/foo_test.rb && echo "all tests pass" # => all tests pass
For a real-world example of TMF in action, have a look at these uses of TMF in the wild:
- test files and Rakefile for ItuCodes
- test files and Rakefile for Gauntlt