Ruby testing libraries

There are a lot of Ruby testing libraries out there and you probably use one of them every day, so it’s about time we dive in to see how they work. This week, the challenge is to create your very own testing library.

Your library should be as tiny as possible – it should be a single file –, but it should have one unique feature you’ve been missing while working with other testing libraries. Of course, it should have a great name and a README, explaining how it works (with code examples) and why it’s awesome.

As always, put the whole thing in a Gist. You can turn it into a gem in a full repository after the contest’s voting period is over. You have one week to get your entry in, so be sure to make something awesome. Good luck!

This contest is finished

Congratulations to this week's winners! The entries and the contestant names are shown below.

  • README
    TestRocket is a simple, tiny testing library for Ruby 1.9.
    
    If => in a hash is a "hash rocket", then +-> and --> for tests should be
    "test rockets"!
    
    Simple syntax:
    
    +-> { block that should succeed }
    --> { block that should fail }
    
    These tests would both pass:
    
    +-> { 2 == 2 }
    --> { 10 / 0 }
    
    See the demonstration test "suite" for more.
    
    Unique features (I hope):
    
    * Unary plus/minus to denote expectations.
    * Ability to fit the entire library into a single tweet/single line.
    testrocket.rb
    # A SINGLE FILE CONTAINING THE TEST LIBRARY
    # A CLASS TO TEST, AND THE TESTS THEMSELVES
    
    # ===========================================================
    # TESTROCKET TESTING LIBRARY
    module TestRocket
      def _test(a, b)
        send((call rescue()) ? a : b)
      end
    
      def +@; puts _test :_pass, :_fail; end
      def -@; puts _test :_fail, :_pass; end
    
      def _pass; '   OK'; end
      def _fail; " FAIL @ #{source_location.join(':')}"; end
    end
    
    Proc.send :include, TestRocket
    
    # A functionally equivalent single liner. I figured you'd rather be able to read the code and output though.
    # class Proc;def _t;call rescue();end;def +@;p _t ? '.' : _f; end;def -@; p _t ? _f : '.'; end;def _f; "F "+to_s;end;end
    
    
    
    
    # ===========================================================
    # "DIE" - Simple dice simulator for us to test
    class Die
      attr_reader :sides
      
      def initialize(sides = 6)
        raise ArgumentError if sides < 2
        @sides = sides
      end  
      
      def roll
        rand(@sides) + 1
      end
    end
    
    
    
    
    # ===========================================================
    # EXAMPLE TEST "SUITE" FOR "DIE"
    #
    # USAGE
    # +-> { block that should succeed }
    # --> { block that should fail }
    
    +-> { Die.new(2) }
    --> { Die.new(1) }
    +-> { (1..6) === Die.new(6).roll }
    +-> { Die.new.sides == 6 }
    
    +-> { die = Die.new(6)
          1000.times { raise unless (1..6) === die.roll } }
    
    +-> { die = Die.new(6)
          1000.times { raise if die.roll.zero? } }
    
    # These two tests will deliberately fail
    +-> { raise }
    --> { true }
    
    View full entry
    Finished in 1st place with a final score of 3.8/5. (View the Gist)
  • README.markdown

    Detective

    What is it?

    Simply put, this is a ruby testing library which asks questions in order to determine compliance.

    That is, you ask the user questions with "ask", and test results with "expect". You can also give instructions with "instruct".

    The entire testing suite is evaluated using the command-line. e.g.

    $ detective ./example_tests.rb
    
    We should follow instructions.
    Follow instructions, press enter [press RETURN to continue]
    
    Enter 5: 
    5
    

    How do i write my tests?

    In a similar fashion to rspec, you put your tests in "describe" or "it" blocks.

    describe "Cool feature" do
      instruct("We will be testing this cool feature")
      it "should show the correct word" do
        instruct("Go to coolwebapp.com")
        ask("What does it say in the middle of the front page?").expect("Cheezburger")
      end
    end
    

    How do i run it?

    detective tests1.rb tests2.rb ...
    

    Have fun!

    example_tests.rb
    # Our cool tests
    describe "We should follow instructions" do
      instruct("Follow instructions, press enter")
      ask("Enter 5").expect(5)
      
      it "even if they dont make sense" do
        instruct("Enter 22 instead of 1")
        ask("Enter 1").expect(22)
      end
    end
    
    describe "More tests" do
      instruct("We will now ask you several more questions")
      ask("Enter lolcat").expect('lolcat')
      it "should drive the user crazy" do
        ask("Are you crazy yet?").expect('yes')
      end
      ask("Do you want me to ask you another question?").expect('yes')
      instruct("I cant think of another question, goodbye!")
    end
    
    detective.rb
    #!/usr/bin/env ruby
    # detective
    # Tests by asking the user questions
    #
    # NOTE: requires ruby 1.9.2 and the rainbow gem.
    #
    # Usage:
    #   detective [list of ruby test files]
    #
    # Syntax:
    #   describe "A test" do
    #     instruct "Go to google"
    #     ask("What is written on the first button?").expect("Google Search")
    #   end
    #
    #
    require 'fiber'
    require 'rainbow'
    
    module Detective
      # Ask question. e.g.
      #   ask "Where were you last night?"
      def ask(question)
        @last_question = question
        @last_result = Fiber.yield [:ask, question]
        return self
      end
    
      # Instruct the user. e.g.
      #    instruct "You have the right to remain silent"
      def instruct(question)
        Fiber.yield [:instruct, question]
        return self
      end
    
      # Expect a result from the last question. e.g.
      #    ask("When did you last see M. Ratchett alive?").expect("last evening")
      def expect(result)
        if (@last_result||'').strip != result.to_s.downcase
          Fiber.yield [:fail, @last_question, @last_result, result]
        end
      end
      
      # Describe a test. e.g.
      #    describe("The suspect") { ask("How many fingers am i holding up?").expect(1) }
      def describe test, &block
        Fiber.yield [:describe, test]
        yield
        Fiber.yield [:describe_end, test]
      end
      
      alias_method :it, :describe
    end
    
    def run_suite(files)
      fib = Fiber.new do
        include Detective
        files.each { |f| require f }
      end
      
      total_test = 0
      total_pass = 0
      test_results = []
    
      begin
        result = nil
        describe_stack = []
        while (command = fib.resume(result)) and !command.nil? and command != :end
          result = nil
          case command[0]
          when :instruct
            if describe_stack[-1][1]
              puts "#{command[1]} [press RETURN to continue]"
              result = STDIN.readline
            end
          when :ask
            if describe_stack[-1][1]
              puts "#{command[1]}: "
              result = STDIN.readline.downcase
            end
          when :describe
            describe_stack << [command[1], true]
            puts "\n#{describe_stack.map{|d|d[0]}.join(' ')}.\n"
            total_test += 1
          when :describe_end
            test_results << ["#{describe_stack.map{|d|d[0]}.join(' ')}.", describe_stack[-1][1]]
            total_pass += 1 if describe_stack.pop[1]
          when :fail
            describe_stack[-1][1] = false
          end
        end
      rescue FiberError
      rescue Exception => e
        puts e.inspect
        puts e.backtrace
      end
      
      puts "\n"
      test_results.reverse.each do |test|
        puts test[0].color(test[1] ? :green : :red)
      end
      puts "\n#{total_pass} / #{total_test} Tests passed".color(total_pass == total_test ? :green : :red)
      
      exit total_pass == total_test ? 0 : 1
    end
    
    run_suite(ARGV)
    
    View full entry
    Finished in 2nd place with a final score of 3.3/5. (View the Gist)
  • Readme.md

    Classy — Class Testing

       classy is simple
    no setup or descriptions
        just equality
    

    (ruby 1.9 only, probably)

    test.rb

    require './classy'
    
    str = Classy "ruby string"
    str.size == 11
    str.size != 11
    str.reverse == "what"
    
    ary = Classy [3,1,2]
    ary.size == 3
    ary.size != 9
    ary.sort == [1,2,3]
    ary << 4 == [8]
    

    output

    $ ruby test.rb
    .FF...F
    Expected not 11, but it was 11
        from test.rb:5:in `<main>'
    
    Expected "what", but it was "gnirts ybur"
        from test.rb:6:in `<main>'
    
    Expected [8], but it was [3, 1, 2, 4]
        from test.rb:12:in `<main>'
    
    7 tests, 4 passing, 3 failing
    0.000182 seconds elapsed
    
    example.rb
    require "./classy"
    
    class Adder
      def initialize(a)
        @a = a
      end
    
      def add(b)
        @a + b
      end
    end
    
    
    a = Classy Adder.new(4)
    a.add(2) == 6
    a.add(0) == 4
    a.add(0) == 2
    a.add(0) != 2
    
    str = Classy "ruby string"
    str.size == 11
    str.size != 11
    str.reverse == "what"
    
    ary = Classy [3,1,2]
    ary.size == 3
    ary.size != 9
    ary.sort == [1,2,3]
    ary << 4 == [8]
    
    classy.rb
    module Classy
      def Classy(obj)
        TestClass.new(obj)
      end
    
      def self.included(klass)
        Runner.start
      end
    
      class TestClass < BasicObject
        def initialize(obj)
          @obj = obj
        end
    
        def method_missing(method, *args, &blk)
          TestResponse.new @obj.send(method, *args, &blk)
        end
      end
    
      class TestResponse < Struct.new(:actual)
        def ==(expected)
          test expected, true
        end
    
        def !=(expected)
          test expected, false
        end
    
        private
        def test(expected, truth)
          if (actual == expected) == truth
            Runner.pass
          else
            Runner.fail actual, expected, caller, truth
          end
        end
      end
    
      module Runner
        extend self
        @@pass = 0
        @@failures = []
    
        def pass
          print "."
          @@pass += 1
        end
    
        def fail(actual, expected, source, truth)
          print "F"
          @@failures << "Expected #{'not ' unless truth}#{expected.inspect}, but it was #{actual.inspect}\n\tfrom #{source[1]}"
        end
    
        def start
          @@start = Time.now
          at_exit { finish }
        end
    
        def finish
          @@failures.each {|f| puts; puts f }
          puts "\n#{@@pass + @@failures.size} tests, #{@@pass} passing, #{@@failures.size} failing"
          puts "#{Time.now - @@start} seconds elapsed"
        end
      end
    end
    
    include Classy
    
    View full entry
    Finished in 3rd place with a final score of 3.2/5. (View the Gist)
  • Readme.md

    a

    a is a testing framework written in 6 lines of code (or 472 characters) which tries to be performant, with eye-catchy reports and easy to use.

    Heavily inspired by Konstantin Haase's almost-sinatra, its long-term purpose is to become the fastest testing framework available.

    The idea is to stay under 7 lines with less than 80 chars per line. There is room for optimization!

    Features

    • Setup / Teardown
    • Assertions (using the a method)
    • Report tests/assertions/failures
    • Keep track of lines where failures happened

    Usage

    Just clone this gist and run:

    ruby example.rb
    

    And voilà:

    a

    example.rb
    require 'a'
    
    class MyTestCase < A
      def setup
        @user = { :some => :object }
      end
    
      def test_user_has_property
        a @user[:some] == :object
        a !@user[:other]
      end
      
      def test_user_not_nil
        a !@user.nil?
      end
    end
    
    class MyOtherTestCase < A
      def setup
        @foo = [1,2,3]
      end
    
      def test_user_has_property
        a @foo.length == 3
        a @foo[2] > 934 # Should fail at line 27
    
        @foo[1] = 99
    
        a @foo[1] != 2
      end
    
      def teardown
        @user = nil
      end
    end
    
    a.rb
    $n=Time.now;$f=[];$c=[];$a=0;class A;def a c;$a+=1;print c ?"\e[32m.\e[0m":($f\
    <<caller[0];"\e[31mF\e[0m");end;def self.inherited(b);$c<<b;end;end;at_exit{$c\
    .map{|t|i=t.new;i.setup rescue nil;$t=t.instance_methods(false).map{|m|i.send\
    m if !%w(setup teardown).include? m};i.teardown rescue nil};print "\n#{Time.\
    now-$n} s.\n#{$t.size} tests, #{$a} assertions, #{$f.size} failures\n\n"+$f.\
    map{|f|"\e[31mFailure\e[0m \e[97m@\e[33m #{f}"}.join("\n");exit -1 if $f[0]}
    
    View full entry
    Finished in 4th place with a final score of 3.2/5. (View the Gist)
  • sample.rb
    require 'supispec'
    
    describe 'My Application' do
      describe 'Hello World' do
        it 'should be true' do
          'hello' != 'world'
        end
        
        it 'should not be true' do
          'hello' == 'world'
        end
      end
    end
    
    supispec.rb
    #!/usr/bin/env ruby
    
    if RUBY_PLATFORM.downcase.include?("mswin") || RUBY_PLATFORM.downcase.include?("mingw")
      require 'win32console'
    end
    
    $indent_level = 0
    
    class SupiSpec
      def initialize
        @tests = []
      end
      
      def print
        @tests.each do |t|
          out = case t[1]
          when :fail then red(t[0])
          when :pass then green(t[0])
          end
          puts ('  ' * $indent_level) + out
        end
      end
      
      private
      
      def it(description, &block)
        if block.call
          @tests << [description, :pass]
        else
          @tests << [description, :fail]
        end
      end
      
      def colorize(text, color_code)
        "\e[#{color_code}m#{text}\e[0m"
      end
      
      def red(text); colorize(text, 31); end
      def green(text); colorize(text, 32); end
      
    end
    
    def describe(description, &block)
      puts ('  ' * $indent_level) + description + ':'
      $indent_level += 1
      test = SupiSpec.new
      test.instance_eval &block
      test.print
      $indent_level -= 1
    end
    
    README.textile

    SupiSpec

    SupiSpec uses a rspec like DSL, but eliminates the need of assertions, or, in
    rspecs case, #should. I know it’s ugly with this global variable, but I couln’t
    find a nicer solution within the span of my motivation :)

    Sample

    Find a sample in sample.rb

    View full entry
    Finished in 5th place with a final score of 3.2/5. (View the Gist)
  • README.md

    This simple framework allows you to assert things that should happen tomorrow*. For example:

    assert_tomorrow { Time.now.wday == 2 } # => :passed
                                           # assuming today is Monday (i.e. wday == 1)
    

    Another example shows that the time will never be the same again:

    today = Time.now
    assert_tomorrow { Time.now == today } # => :failed
    

    And sometimes we can't event assert something for sure, because my expectation is too complex!

    assert_tomorrow { _why is back }      # => :error
    

    Of course, there is a lot room for improvement. You can introduce you own DSL and run tests against your life, for example:

    me = Dnnx.instance.clone
    assert_tomorrow do 
      future_me = Dnnx.instance 
      future_me.is_better_than me
    end
    

    *I didn't invent a time machine, so you have to wait 1 day for test result.

    assert_tomorrow.rb
    def assert_tomorrow() sleep(24*60*60); (yield ? :passed : :failed) rescue :error end
    
    View full entry
    Finished in 6th place with a final score of 2.4/5. (View the Gist)