From: Jesse Jones Date: 2001-12-20T00:07:09+09:00 Subject: [ruby-talk:28935] Re: Yet Another Unit Test Framework In article <3C1F84D9.71C94118@stud.ntnu.no>, Kent Dahl wrote: > Jesse Jones wrote: > > Most tests are fundamentally pretty simple: you create one or more > > objects, call some methods, and verify that the object state is what > > you expected. There's no need to create a whole new class to do this, a > > class method is more than adequate. > > But the class method has a much larger definition signature in Ruby, > which I don't like. Thou tests are simple, I do use classes to structure > them. I found it very "wierd" to have them in a class called UnitTest as > class methods. > > You also end up writing this kind of "scope" into the test method name: > test_String_substr > This makes my backhead scream for a TestString class. I rewrote the framework to use classes and it turned out pretty well. It solved this problem and as a bonus resulted in a more cohesive UnitTest class. I have to say that this code is pretty darned cool, it's way nicer than similar sorts of things I've written in C++. :-) The client code is pretty similar to the previous code: class TestString < TestCase def test_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)} text = "ab" test_result {text.substr(0) == "ab"} # and so on end end The UnitTest class is a lot simpler since much of the work has been pushed off onto the new TestCase class: ###################################################################### class UnitTest private @@tests = [] public # Call this to run all the test cases. You can adjust the timeout # value if you have a test case that takes an exceptionally long # time to execute. def UnitTest::run_all(seconds = 10) do_run_all(seconds) end # Call this to run an individual test case: # UnitTest::run_test(TestString) def UnitTest::runTest(name, seconds = 10) do_run_test(name, seconds) end # Internal method called by TestCase to register the test cases. def UnitTest::add_test(test) @@tests.push(test) end private def UnitTest::do_run_all(seconds) count, failures = 0, 0 @@tests.each do |name| failures += do_run_test(name, seconds) count += 1 end if failures == 0 puts "ran #{count} test cases" elsif failures == 1 puts "ran #{count} test cases, with 1 failure" else puts "ran #{count} test cases, with #{failures} failures" end end def UnitTest::do_run_test(name, seconds) test = name.new test.setup failures = test.run(seconds) test.teardown return failures end end ###################################################################### # Base class for unit tests. Subclass TestClass and add public test # methods that start with "test". class TestCase # Optional method that is called before any of the "test*" methods. def setup end # Optional method that is called after the "test*" methods execute. def teardown end # Use this within a test to test arbitrary predicates: # test_result {10 > 2} def test_result(&expr) do_test_result(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 test_failed(error) printf("\n\t\t%s (%s)", error, caller[0]) @failures += 1 end # Use this to verify that a methods precondition checks are working: # test_assert {object[object.length + 100]} def test_assert(&expr) do_test_exception(AssertError, expr) end # Use this to verify that a method is throwing an exception for # error conditions: # test_exception(ParserError) {parser.parse("garbage")} def test_exception(e, &expr) do_test_exception(e, expr) end # Runs all of the "test*" methods in the object. Clients should use # UnitTest::runTest() instead of calling this method directly. def run(seconds) failures = do_run(seconds) return failures end # Hook that is called when a new subclass is created. def TestCase::inherited(subclass) UnitTest::add_test(subclass) end private def do_run(seconds) failures = 0 methods = public_methods.sort text = type.to_s text = text.slice("Test".length, text.length - "Test".length) if text.index("Test") == 0 puts "Testing #{text}:" methods.each do |name| if name.index("test") == 0 && name != "test_result" && name != "test_assert" && name != "test_failed" && name != "test_exception" @failures = 0 begin # 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 text = text.slice(1, text.length - 1) if text[0] = ?_ printf("\t#{text}...") # execute the test, timeout(seconds) {send(name)} # and print the results. if @failures == 0 printf("done\n") else printf("\n"); end rescue TimeoutError printf("\n\t\t#{name} took longer than #{seconds} seconds!\n") @failures += 1 rescue Exception => e printf("\n\t\tCaught a '#{e}' exception while running the test:\n\t\t#{e.backtrace.join(\"\n\t\t\")}\n") @failures += 1 end failures += @failures end end return failures end def do_test_exception(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\t%s (%s)", error, caller[1]) @failures += 1 end end def do_test_result(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\t%s (%s)", error, caller[1]) @failures += 1 end end end -- Jesse