From: Jesse Jones Date: 2001-12-17T18:36:41+09:00 Subject: [ruby-talk:28747] Yet Another Unit Test Framework I've just started working with Ruby. I'm a hardcore C++ programmer, but I've never used a dynamic language before so it's been an interesting few days. :-) One of the first things I did was write a unit testing framework. I think it turned out really well: it's very easy to use and seems a lot simpler than rubyunit. I'm curious to know what more experienced Ruby programmers think about it so I'll present it here. Feel free to point out any areas that could be improved (even stylistic issues). First I'll show how the unit tests work from the POV of clients. Here's a method that we want to test: require "Debugging.rb" # String.substr works like std::string::substr # substr(index) returns the sub-string starting at index # substr(index, count) returns count chars starting at index class String def substr(index, *count) precondition(index >= 0 && index < length) precondition(count.length <= 1) precondition(count.empty? || index+count[0] <= length) if count.empty? return slice(index, length - index) else return slice(index, count[0]) end end end In order to test it we have to add a new method to the UnitTest class. The method name must start with "test_". We use test_result() for tests that should succeed and test_assert() for tests that should trigger precondition violations. To run the test we can use UnitTest:: runTest("test_String_substr") or UnitTest::test() which will run all of the tests. # String unit tests class UnitTest def UnitTest.test_String_substr text = "" test_assert {text.substr(0)} text = "a" test_result {text.substr(0) == "a"} test_result {text.substr(0, 1) == "a"} test_assert {text.substr(2)} test_assert {text.substr(-1)} test_assert {text.substr(0, 1, 1)} test_assert {text.substr(0, 10)} # more tests go here end end Here's the unit test code. It should be fairly self-explanatory. require "timeout" ###################################################################### # AssertError is raised by Object.assert, Object.precondition and # Object.postcondition class AssertError < RuntimeError end ###################################################################### # Add some debugging methods to Object: # precondition takes a predicate that clients of a method have to # ensure is valid # postcondition takes a predicate that a method guarantees is valid # assert takes a predicate that is used for internal sanity checks class Object def assert(expr) if !expr raise AssertError, "assertion failure", caller end end alias precondition assert alias postcondition assert end ###################################################################### # UnitTest is used to hold all the tests in an application. Each test # should be a class method and the method name must start with "test_". # It's a good idea to embed the class name, and optionally the method, # of what you are testing into the method name, eg test_Vector or # test_Matrix_invert. class UnitTest public # Use this within a unit test to test arbitrary predicates: # test_result {10 > 2} def UnitTest::test_result(&expr) doTestResult(expr) end # Use this to signal a test failure: # value = # a big long expression # test_failed("Value isn't 0! It's #{value.to_s}.") if value != 0 def UnitTest::test_failed(error) printf("\n\t%s (%s)", error, caller[0]) @@test_failures += 1 end # Use this to verify that a method's precondition checks are # working: test_assert {object[object.length + 100]} def UnitTest::test_assert(&expr) doTestException(AssertError, expr) end # Use this to verify that a method is throwing an exception for # error conditions: test_exception(ParserError) # {parser.parse("garbage")} def UnitTest::test_exception(e, &expr) doTestException(e, expr) end # Call this to run all the unit tests. You can adjust the timeout # value if you have a unit test that takes an exceptionally long # time to execute. def UnitTest::test(seconds = 10) doTest(seconds) end # Call this to run an individual unit test: # UnitTest::runTest("test_CoolClass_uberMethod") def UnitTest::runTest(name, seconds = 10) doRunTest(name, seconds) end private @@test_failures = 0 def UnitTest::doRunTest(name, seconds) ranTest = false @@test_failures = 0 begin # If the name is a unit test method then, if name.index("test_") == 0 && name != "test_result" && name != "test_assert" && name != "test_failed" && name != "test_exception" ranTest = true # print the name of the test we are running, text = name.slice("test_".length, name.length - "test_".length) # would be nice to use substr, but we want to avoid a dependency upon StringUtils.rb here index = text.rindex("_") if index clss = text.slice(0, index) method = text.slice(index+1, text.length - index+1) printf("running the #{clss}.#{method} test...") else printf("running the #{text} test...") end # execute the test, timeout(seconds) {send(name)} # and print the results. if @@test_failures == 0 printf("done\n") elsif @@test_failures == 1 printf("\n1 test failed\n") else printf("\n%d tests failed\n", @@test_failures); end end rescue TimeoutError printf("\n#{name} took longer than #{seconds} seconds!\n") @@test_failures += 1 rescue Exception => e printf("\nCaught a '#{e}' exception while running the unit test:\n\t#{e.backtrace.join(\"\n\t\")}\n") @@test_failures += 1 end return ranTest end def UnitTest::doTest(seconds) methods = UnitTest.singleton_methods.sort count, failures = 0, 0 methods.each do |name| if doRunTest(name, seconds) count += 1 failures += 1 if @@test_failures != 0 end end if failures == 0 puts "ran #{count} tests" elsif failures == 1 puts "ran #{count} tests, with 1 failure" else puts "ran #{count} tests, with #{failures} failures" end end def UnitTest::doTestException(e, expr) error = "" begin expr.call error = "Expected a #{e.to_s}!" rescue e # all is well rescue Exception => x error = "Got an exception (#{x}), but it wasn't a #{e.to_s} exception!" end if !error.empty? printf("\n\t%s (%s)", error, caller[0]) @@test_failures += 1 end end def UnitTest::doTestResult(expr) error = "" begin error = "test_result evaluated as false!" if !expr.call rescue AssertError error = "Got an unexpected assertion!" rescue Exception => x error = "Got an unexpected exception (#{x})!" end if !error.empty? printf("\n\t%s (%s)", error, caller[0]) @@test_failures += 1 end end end -- Jesse