Test::Unit Given

Author

Dave Copeland (davetron5000 at g mail dot com)

Copyright

Copyright © 2011 by Dave Copeland

License

Distributes under the Apache License, see LICENSE.txt in the source distro

Get your Test::Unit test cases fluent, without RSpec, magic, or crazy meta-programming.

This gives you some simple tools to make your test cases readable:

  • Given/When/Then to delineate which parts of your tests do what

  • A method “test_that” that defines test cases with strings instead of method names (much like test in Rails tests)

  • A module Any that defines methods to create arbitrary values, so it’s clear in your tests what values are what, but without having to use long method names like ‘Faker::Lorem.words(2).join(’ ‘)`

Install

gem install test_unit-given

‘test_that`

class SomeTest < Test::Unit::Given::TestCase
  test_that {
    # do a test
  }

  test_that "description of our test" do
    # do a test
  end
end

Yes, just like ‘test` from Rails, but without having to have ActiveSupport

Given/When/Then

class Circle
  def initialize(radius)
    @radius = radius
  end

  def area
    @radius * @radius * 3.14
  end

end

require 'test/unit/given'

class CircleTest < Test::Unit::Given::TestCase
  test_that {
    Given {
      @circle = Circle.new(10)
    }
    When {
      @area = @circle.area
    }
    Then {
      assert_equal 314,@area
    }
  }
end

You can, of course, provide a description for your test if you need to:

class CircleTest < Test::Unit::Given::TestCase
  test_that "the area is correctly calculated" do
    Given {
      @circle = Circle.new(10)
    }
    When {
      @area = @circle.area
    }
    Then {
      assert_equal 314,@area
    }
  end
end

You can use as much, or as little, of this library as you want. If you don’t like test_that, it’s no problem:

class CircleTest < Test::Unit::TestCase
  include Test::Unit::Given::Simple

  def test_that_area_is_calculated
    Given {
      @circle = Circle.new(10)
    }
    When {
      @area = @circle.area
    }
    Then {
      assert_equal 314,@area
    }
  end
end

If you just want to use the Given, you can:

class CircleTest < Test::Unit::TestCase
  include Test::Unit::Given::Simple

  def test_that_area_is_calculated
    Given {
      @circle = Circle.new(10)
    }
    area = @circle.area
    assert_equal 314,@area
  end
end

Feel a Given is too verbose?

class CircleTest < Test::Unit::TestCase
  include Test::Unit::Given::Simple

  def test_that_area_is_calculated
    When {
      @area = Circle.new(10).area
    }
    Then {
      assert_equal 314,@area
    }
  end
end

Use whatever makes sense; this is here to make your tests readable and communicate your intent, not lock you into some particular way of writing your tests.

How does it work?

Given/When/Then/And/But are all the same method under the covers. They take a block and execute it immediately. By using instance variables, you can send information between blocks. This is actually a feature, since it means than any instance variables are important while local variables are just there to set up your test or help evalulate things.

This means that you can make methods that return blocks as a means of re-use.

class CircleTest < Test::Unit::Given::TestCase

  def circle_with_radius(r)
    lambda { @circle = Circle.new(r) }
  end

  def get_area
    lambda { @area = @circle.area }
  end

  def area_should_be(area)
    lambda { assert_equal area,@area }
  end

  test_that {
    Given circle_with_radius(10)
    When get_radius
    And {
      @diameter = @circle.diameter
    }
    Then area_should_be(314)
    And {
      assert_equal 20,@diameter
    }
  }
end

I would not recommend doing this a lot, as it can make things very confusing. You could just as easily continue to use methods.

test_that works just like test in Rails, except that it doesn’t require a name. If your test is short enough, naming it might make things more confusing. I tend to always name mine, but on occasion it gets in the way.

What about mocks?

Mocks create an interesting issue, because the “assertions” are the mock expectations you setup before you call the method under test. This means that the “then” side of things is out of order.

class CircleTest < Test::Unit::Given::TestCase
  test_that "our external diameter service is being used" do
    Given {
      @diameter_service = mock()
      @diameter_service.expects(:get_diameter).with(10).returns(400)
      @circle = Circle.new(10,@diameter_service)
    }
    When  {
      @diameter = @circle.diameter
    }
    Then {
      // assume mocks were called
    }
  end
end

This is somewhat confusing. We could solve it using two blocks provided by this library, the_test_runs, and mocks_shouldve_been_called, like so:

class CircleTest < Test::Unit::Given::TestCase
  test_that "our external diameter service is being used" do
    Given {
      @diameter_service = mock()
    }
    When the_test_runs
    Then {
      @diameter_service.expects(:get_diameter).with(10).returns(400)
    }
    Given {
      @circle = Circle.new(10,@diameter_service)
    }
    When  {
      @diameter = @circle.diameter
    }
    Then mocks_shouldve_been_called
  end
end

Although both the_test_runs and mocks_shouldve_been_called are no-ops, they allow our tests to be readable and make clear what the assertions are that we are making.

Yes, this makes our test a bit longer, but it’s much more clear.

What about block-based assertions, like assert_raises

Again, things are a bit out of order, but if you invert Then and When, you’ll still get a readable test:

class CircleTest < Test::Unit::Given::TestCase

  test_that "there is no diameter method" do
    Given {
      @circle = Circle.new(10)
    }
    Then {
      assert_raises NoMethodError do
        When {
          @circle.diameter
        }
      end
    }
  end
end

Any

Our tests tend to have a lot of arbitrary or meaningless values. They also have a lot of very important and meaningfule values. Often both of these are expressed as literals in our code. What the Any module gives you is the ability to hide arbitrary values behind a method call. This will ensure that the values truly are arbitrary, and will also mean that any literals left in your tests are important. For example, you might have a test like this:

def test_something
  service = mock()
  service.expects(:remote_call).with({ :method => 'process', :amount => 45.6}).returns(87)
  object_under_test = ObjectUnderTest.new('foo',service)

  assert_equal 8700,object_under_test.doit
end

What’s being tested here? What values are meaningfule and which aren’t? Let’s apply Any to make it clear:

def test_something
  service = mock()
  service_return = any_int
  service.expects(:remote_call).with({ :method => any_string, :amount => any_number}).returns(service_return)
  object_under_test = ObjectUnderTest.new(any_string,service)

  assert_equal (service_return * 100),object_under_test.doit
end

Now it’s clear that we’re expecting our object under test to multiple the return of our service call by 100. The only value that has any meaning to this test is the integer 100, and that’s the only literal that’s there. Beauty.

What about Faker or Sham?

They simply don’t provide a concise API to do this, nor do they really communicate this concept. We aren’t passing fake strings or numbers, we’re passing arbitrary strings and numbers. It’s worth making that clear in our tests that certainly values that must not be nil, don’t matter to the test. That they are random each time keeps us honest.

What if I need some sort of restriction?

Two ways to do that. The built-in any_* methods provide limited options:

def test_truncate
  person = Person.new
  person.name = any_string :min => 256
  person.age = any_int :positive
  person.save!
  assert_equal 255,person.name.length
end

The second way is to create your own any:

def setup
  new_any :age do |options|
    age = any_number % 80
    age += 18 if options == :adult
    age
  end
end

def test_truncate
  person = Person.new
  person.name = any_string :min => 256
  person.age = any :age, :adult
  person.save!
  assert_equal 255,person.name.length
end

WTF? Why?

Just because you’re using Test::Unit doesn’t mean you can’t write fluent, easy to understand tests. You really don’t need RSpec, and RSpec has some baggage, such as nonstandard assignment, confusing class_eval blocks, and generally replaces stuff you can do in plain Ruby. Here, everything is simple, plain Ruby. No magic, nothing to understand.

If you like Test::Unit, and you want to make your tests a bit more readable, this is for you.