Unit testing is a great way to catch errors early in the development process, if you dedicate time to writing appropriate and useful tests. As in other languages, Ruby provides a framework in its standard library for setting up, organizing, and running tests called Test::Unit.
There are other very popular testing frameworks, rspec and cucumber come to mind.
Specifically, Test::Unit provides three basic functionalities:
First create a new class.
# File: simple_number.rbclassSimpleNumberdefinitialize(num)raiseunlessnum.is_a?(Numeric)@x=numenddefadd(y)@x+yenddefmultiply(y)@x*yendend
Let's start with an example to test the SimpleNumber class.
# File: tc_simple_number.rbrequire_relative"simple_number"require"test/unit"classTestSimpleNumber<Test::Unit::TestCasedeftest_simpleassert_equal(4,SimpleNumber.new(2).add(2))assert_equal(6,SimpleNumber.new(2).multiply(3))endend
Which produces
>> ruby tc_simple_number.rb Loaded suite tc_simple_number Started . Finished in 0.002695 seconds. 1 tests, 2 assertions, 0 failures, 0 errors
So what happened here? We defined a classTestSimpleNumber which inherited fromTest::Unit::TestCase. InTestSimpleNumber we defined a member function calledtest_simple. That member function contains a number of simpleassertions which exercise my class. When we run that class (note I haven't put any sort of wrapper code around it -- it's just a class definition), the tests are automatically run, and we're informed that we've run 1 test and 2 assertions.
Let's try a more complicated example.
# File: tc_simple_number2.rbrequire_relative"simple_number"require"test/unit"classTestSimpleNumber<Test::Unit::TestCasedeftest_simpleassert_equal(4,SimpleNumber.new(2).add(2))assert_equal(4,SimpleNumber.new(2).multiply(2))enddeftest_typecheckassert_raise(RuntimeError){SimpleNumber.new('a')}enddeftest_failureassert_equal(3,SimpleNumber.new(2).add(2),"Adding doesn't work")endend
>> ruby tc_simple_number2.rbLoaded suite tc_simple_number2StartedF..Finished in 0.038617 seconds. 1) Failure:test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]:Adding doesn't work.<3> expected but was<4>.3 tests, 4 assertions, 1 failures, 0 errors
Now there are three tests (three member functions) in the class. The functiontest_typecheck usesassert_raise to check for an exception. The functiontest_failure is set up to fail, which the Ruby output happily points out, not only telling us which test failed, but how it failed (expected <3> but was <4>). On this assertion, we've also added an final parameters which is a custom error message. It's strictly optional but can be helpful for debugging. All of the assertions include their own error messages which are usually sufficient for simple debugging.
Test::Unit provides a rich set of assertions, which are documented thoroughly atRuby-Doc. Here's a brief synopsis (assertions and their negative are grouped together. The text description is usually for the first one listed -- the names should make some logical sense):
| assert( boolean, [message] ) | True ifboolean |
| assert_equal( expected, actual, [message] ) assert_not_equal( expected, actual, [message] ) | True ifexpected == actual |
| assert_match( pattern, string, [message] ) assert_no_match( pattern, string, [message] ) | True ifstring =~ pattern |
| assert_nil( object, [message] ) assert_not_nil( object, [message] ) | True ifobject == nil |
| assert_in_delta( expected_float, actual_float, delta, [message] ) | True if(actual_float - expected_float).abs <= delta |
| assert_instance_of( class, object, [message] ) | True ifobject.class == class |
| assert_kind_of( class, object, [message] ) | True ifobject.kind_of?(class) |
| assert_same( expected, actual, [message]) assert_not_same( expected, actual, [message] ) | True ifactual.equal?( expected ). |
| assert_raise( Exception,... ) {block} assert_nothing_raised( Exception,...) {block} | True if the block raises (or doesn't) one of the listed exceptions. |
| assert_throws( expected_symbol, [message] ) {block} assert_nothing_thrown( [message] ) {block} | True if the block throws (or doesn't) the expected_symbol. |
| assert_respond_to( object, method, [message] ) | True if the object can respond to the given method. |
| assert_send( send_array, [message] ) | True if the method sent to the object with the given arguments return true. |
| assert_operator( object1, operator, object2, [message] ) | Compares the two objects with the given operator, passes iftrue |
Tests for a particular unit of code are grouped together into atest case, which is a subclass of Test::Unit::TestCase. Assertions are gathered intests, member functions for the test case whose names start withtest_. When the test case is executed or required, Test::Unit will iterate through all of the tests (finding all of the member functions which start withtest_ using reflection) in the test case, and provide the appropriate feedback.
Test case classes can be gathered together intotest suites which are Ruby files which require other test cases:
# File: ts_all_the_tests.rbrequire'test/unit'require'test_one'require'test_two'require'test_three'
In this way, related test cases can be naturally grouped. Further, test suites can contain other test suites, allowing the construction of a hierarchy of tests.
This structure provides relatively fine-grained control over testing. Individual tests can be run from a test case (see below), a full test case can be run stand-alone, a test suite containing multiple cases can be run, or a suite of suites can run, spanning many test cases.
The author of Test::Unit, Nathaniel Talbott, suggests starting the names of test cases withtc_ and the names of test suites withts_
It's possible to run just one (or more) tests out of a full test case:
>> ruby -w tc_simple_number2.rb --name test_typecheck Loaded suite tc_simpleNumber2Started.Finished in 0.003401 seconds.1 tests, 1 assertions, 0 failures, 0 errors
It is also possible to run all tests whose names match a given pattern:
>> ruby -w tc_simple_number2.rb --name /test_type.*/ Loaded suite tc_simpleNumber2Started.Finished in 0.003401 seconds.1 tests, 1 assertions, 0 failures, 0 errors
There are many cases where a small bit of code needs to be run before and/or after each test. Test::Unit provides thesetup andteardown member functions, which are run before and after every test (member function).
# File: tc_simple_number3.rbrequire"./simple_number"require"test/unit"classTestSimpleNumber<Test::Unit::TestCasedefsetup@num=SimpleNumber.new(2)enddefteardown## Nothing reallyenddeftest_simpleassert_equal(4,@num.add(2))enddeftest_simple2assert_equal(4,@num.multiply(2))endend
>> ruby tc_simple_number3.rbLoaded suite tc_simple_number3Started..Finished in 0.00517 seconds.2 tests, 2 assertions, 0 failures, 0 errors
Implement a class with a public method that can solve the following problem: A user has a rubber bicycle tire of an arbitrary circumference. When one side is cut, and the bike tire is stretched into a line, it is measured at an arbitrary length. From this length, determine the radius of the bike tire originally.
After this, write a test case to test the class. It should include at least 2 methods to test.