= Test::Unit Given
Author:: Dave Copeland (mailto:davetron5000 at g mail dot com)
Copyright:: Copyright (c) 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 two 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)
== Install
gem install test_unit-given
== Example
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
If you don't want to extend our base class, you can mix in the features explicitly:
require 'test/unit/given/simple'
class CircleTest < Test::Unit::TestCase
include Test::Unit::Given::Simple
include Test::Unit::Given::TestThat
test_that {
Given {
@circle = Circle.new(10)
}
When {
@area = @circle.area
}
Then {
assert_equal 314,@area
}
}
end
Finally, you can re-use blocks, and use And to create richer expressions:
class CircleTest < Test::Unit::Given::TestCase
def circle_with_radius(r)
@circle = Circle.new(r)
end
def get_area
@area = @circle.area
end
def area_should_be(area)
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
=== 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)
@cirlce = Circle.new(10,@diameter_service)
}
When {
@diameter = @cirlce.diameter
}
Then {
// assume mocks were called
}
end
end
This is somewhat confusing. We could solve it like so:
class CircleTest < Test::Unit::Given::TestCase
test_that "our external diameter service is being used" do
Given {
@diameter_service = mock()
}
When mocks_are_called
Then {
@diameter_service.expects(:get_diameter).with(10).returns(400)
}
Given {
@cirlce = Circle.new(10,@diameter_service)
}
When {
@diameter = @cirlce.diameter
}
Then mocks_shouldve_been_called
end
end
Although both mocks_are_called 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.
=== What about block-based assertions, like +assert_raises+
class CircleTest < Test::Unit::Given::TestCase
test_that "there is no diameter method" do
Given {
@cicle = Circle.new(10)
}
Then {
assert_raises NoMethodError do
When {
@cirlce.diameter
}
end
}
end
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.