Tst

A small testing library that tries not to do too much.

You get a top-level tst method, a few assert methods, and readable test output.

Installation

Standard issue:

gem install tst

Usage

The tst method is available on the top-level, takes an optional name and a required block, like so:

tst "Array-indexing is not 1-based" do
  arr = [1, 2, 3, 4, 5]
  assert arr[1] != 1
end

If the block runs to completion without raising anything, it passes.

If one of assertion methods (see below) raises, it counts as a failure.

If the block raises for any other reason, it's an exception.

Assertions

Within a tst block, you have access to 3 assert methods: assert, assert_equal, and assert_raises. They do what you'd expect.

If those don't cover your needs, just write your own:

module Tst

  # The assertions live in the Assertions module and are available
  # to the `tst` blocks.
  module Assertions

    # No naming or argument rules, it's just a method.
    # Define it how you please.
    def sums_to_42(*args)
      actual = args.inject(0) { |sum, arg| sum + arg }

      # return without raising to pass
      return if actual == 42

      # Tst::Failure is a subclass of StandardError that takes
      # 3 arguments: Failure.new(message, expected, actual)
      #
      # message - a brief description of the failure
      # expected - the expected value
      # actual - the actual value
      raise Tst::Failure.new("#{args} doesn't sum to 42", 42, actual)
    end
  end
end

# Now just use your custom assertion in a test
tst "some array sums to 42" do
  sums_to_42(40, 1, 1) # This will pass just fine
  sums_to_42(40, 1, 2) # This will fail
end

Running that test will yield output like:

$ tst 'some_array.rb'
F
F1: [40, 1, 2] doesn't sum to 42 - "some array sums to 42"
  exp: 42
  act: 43
* some_array.rb:<lineno>:in `block in <top (required)>'

Running tests

Tst comes with a command-line runner, named "tst".

$ tst
Usage: tst [options] files

  -h, --help       Displays this message

  -I, --libdir     A directory to add to the load path
                   (can be used multiple times: "-I lib -I test"

  -r, --reporter   Specify a reporter to use. "pretty" or "plain".
                   Defaults to "plain"

  files            Specifies which test files to run. Is passed through to
                   Dir.glob, so anything it accepts is valid. NOTE: use
                   quotes to make sure nothing gets swallowed.

You can also run tests programmatically. Here's a sample rake task, for instance:

require 'tst'

desc 'Run the tests'
task :test do
  Tst.run 'test/*.rb',
    :load_paths => ['.', 'lib'],              # same as -I above
    :reporter => Tst::Reporters::Pretty.new   # same as -r above
end

Meanderings

Why aren't there setup and teardown methods?

Having setup and teardown methods makes it harder to look at one test and see what's happening, as it's easy for relevant stuff to be scrolled off the screen, or even hidden in a superclass. And the names are totally generic.

And we're in ruby! You can get what you need by just writing a method with a name that encapsulates what you need. Take a look at "test/plain_reporter.rb" for an example.

Lightness

Tst tries to avoid weight, in various forms:

Weighty Structure

Tst has no grouping - TestCases, Context blocks, etc, because we're in ruby, where such ceremony is unnecessary. It is not required to make the testing tools work, and I'm not sure it yields much benefit. We already put our test code into files with names. Is further taxonomy really necessary?

I believe the lack of ceremony is easier to understand, learn, use, and is nicer to look at.

Conceptual weight

This may just be a personal preference thing, but I don't understand the appeal of the 'english-y' DSL's.

                                   # Test tool concepts
                                   # ------------------
# Compare this...
tst "a new Thing is not empty" do  # :tst
  thing = Thing.new
  assert !thing.empty?             # :assert
end

# ... to this (RSpec):
describe Thing do                  # :describe
  let(:thing) { Thing.new }        # :let

  it "should be empty" do          # :it
    thing.should be_empty          # :should
  end                              # magic :be_* method
                                   # how :thing gets in scope
end

# or with even more magic:
describe Thing do                  # :describe
  it { should be_empty }           # :it
end                                # magic Thing initialization
                                   # implicit :subject
                                   # :should (w/ implicit :subject)
                                   # :be_*

Some argue that the RSpec examples read well, but look at how much you need to know to understand what's going on.

Don't get me wrong. RSpec is a pretty cool implementation of an interesting idea, but I'm not convinced that it's worth the conceptual overhead when you consider what you get.

Code weight

Just looking in the 'lib' directories:

  • test/unit: 6313
  • rspec: 5460
  • minitest: 1210
  • tst: 190

The core of tst's testing engine is really just 91 lines (see 'tst.rb'). The other 99 is IO/reporting code.