Test Tube

Version Yard documentation CI RuboCop License

A test tube to conduct software experiments 🧪

A researcher experimenting with Ruby code

Installation

Add this line to your application's Gemfile:

gem "test_tube"

And then execute:

bundle

Or install it yourself as:

gem install test_tube

Usage

To make TestTube available:

require "test_tube"

Assuming we'd like to experiment on the answer to the Ultimate Question of Life, the Universe, and Everything with the following matcher:

class BeTheAnswer
  def matches?
    42.equal?(yield)
  end
end

A matcher is an object that responds to the matches? method with a block parameter representing the actual value to be compared.

Back to our Ruby experiments, one possibility would be to invoke a whole block of code:

block_of_code = -> { "101010".to_i(2) }

experiment = TestTube.invoke(
  block_of_code,
  isolation: false,
  matcher:   BeTheAnswer.new,
  negate:    false
)

experiment.actual # => 42
experiment.error  # => nil
experiment.got    # => true

An alternative would be to pass directly the actual value as a parameter:

actual_value = "101010".to_i(2)

experiment = TestTube.pass(
  actual_value,
  matcher: BeTheAnswer.new,
  negate:  false
)

experiment.actual # => 42
experiment.error  # => nil
experiment.got    # => true

Matchi matchers

To facilitate the addition of matchers, a collection is available via the Matchi project.

Let's use a built-in Matchi matcher:

gem install matchi
require "matchi"

An example of successful experience:

experiment = TestTube.invoke(
  -> { "foo".blank? },
  isolation: false,
  matcher:   Matchi::Matcher::RaiseException.new(NoMethodError),
  negate:    false
)

experiment.actual # => #<NoMethodError: undefined method `blank?' for "foo":String>
experiment.error  # => nil
experiment.got    # => true

Another example of an experiment that fails:

experiment = TestTube.invoke(
  -> { 0.1 + 0.2 },
  isolation: false,
  matcher:   Matchi::Matcher::Equal.new(0.3),
  negate:    false
)

experiment.actual # => 0.30000000000000004
experiment.error  # => nil
experiment.got    # => false

Finally, an experiment which causes an error:

experiment = TestTube.invoke(
  -> { BOOM },
  isolation: false,
  matcher:   Matchi::Matcher::Match.new(/^foo$/),
  negate:    false
)

experiment.actual # => nil
experiment.error  # => #<NameError: uninitialized constant BOOM>
experiment.got    # => nil

Code isolation

When experimenting tests, side-effects may occur. Because they may or may not be desired, an isolation option is available.

Let's for instance consider this block of code:

greeting = "Hello, world!"
block_of_code = -> { greeting.gsub!("world", "Alice") }

By setting the isolation option to true, we can experiment while avoiding side effects:

experiment = TestTube.invoke(
  block_of_code,
  isolation: true,
  matcher:   Matchi::Matcher::Eql.new("Hello, Alice!"),
  negate:    false
)

experiment.inspect # => <TestTube actual="Hello, Alice!" error=nil got=true>

greeting # => "Hello, world!"

Otherwise, we can experiment without any code isolation:

experiment = TestTube.invoke(
  block_of_code,
  isolation: false,
  matcher:   Matchi::Matcher::Eql.new("Hello, Alice!"),
  negate:    false
)

experiment.inspect # => <TestTube actual="Hello, Alice!" error=nil got=true>

greeting # => "Hello, Alice!"

Contact

Versioning

Test Tube follows Semantic Versioning 2.0.

License

The gem is available as open source under the terms of the MIT License.


This project is sponsored by:
Sashite