Monday, December 11, 2006

JRake, Part 2: Testing

In my previous post I showed how to integrate JRuby/Rake with javac, in a way that eliminated the need to start up a new virtual machine for each compilation. This was fairly straightforward - I used JRuby's high level java integration to load the compiler class from the system classloader, then invoked its compile method.

Next on my list was integration with JUnit, which turned out to be a bit more complicated. I was able to load the main JUnit classes (e.g. org.junit.runner.JUnitCore) using the same high level mechanism as before, but for the actual test classes, I had to use a separate, non-system classloader. That way I could specify the location of my test classes from within the rakefile (as opposed to having to include those directories on the system classpath) plus I could recompile and rerun my tests without having to start up a new virtual machine.

In the end I got it working without too much trouble. The junit method below takes two arguments: the location of the directory containing the test classes, and the classpath to use when running the tests. It then creates a new classloader for the specified classpath, uses it to load the test classes, and passes those test classes into JUnitCore for execution:

def junit(test_class_dir, class_path)

# Append the test_class_dir to the class_path, if necessary
class_path += [test_class_dir] unless class_path.member?(test_class_dir)

# Make sure test_class_dir has trailing slash
unless test_class_dir[-1,1] == '/'
test_class_dir = test_class_dir + '/'
end

# Scan test_class_dir for test class files
class_names = []
FileList["#{test_class_dir}/**/*Test.class"].each do |class_file|
class_names << class_file[test_class_dir.length,
class_file.length - test_class_dir.length - '.class'.length].gsub('/', '.')
end
fail "No test classes found" if class_names.empty?

# Load the test classes via a new classloader
classes = load_classes(class_path, class_names)

# Run the tests
runner = org.junit.runner.JUnitCore.new
runner.add_listener(org.junit.internal.runners.TextListener.new)
result = runner.run(to_java_array(java.lang.Class, classes))
fail("Unit tests failed") unless result.was_successful
end

def load_classes(class_path, class_names)

# Make sure directories have a trailing slash, otherwise URLClassLoader ignores them
class_path.each { |element| element << '/' if FileTest.directory?(element) && element[-1,1] != '/' }

# Convert classpath elements to URLs
urls = to_java_array(java.net.URL, class_path.map { |element| java.net.URL.new('file:' + element) })

# Create a class loader for the specified class path
loader = java.net.URLClassLoader.new(urls)
return class_names.map { |class_name| loader.loadClass(class_name) }

end

Unfortunately, I had to use the internal JUnit TextListener to display the results of the tests, since JRuby doesn't currently support extending existing Java classes in such a way that I could provide my own subclass of RunListener. This will hopefully be fixed in a future version of JRuby.

Interactive JRake

Now, while all these classloading tricks are nice, they don't really help if all we do is start up the virtual machine, run the tests, shutdown, and repeat, since the original goal was minimize those startups. I've therefore created an "interactive" version of JRake, to keep the virtual machine warmed up between tests. All it does is sit in a loop, prompting the user for targets to build, and building them. A sample session looks like this:

jrake shell v0.0

Valid targets:
clean - deletes all generated files
main_compile - compiles the main code
unit_compile - compiles the unit test code
unit_test - runs the units tests
help - displays this help text
exit - quits the application

A blank line repeats the previous command.

jrake> main_compile
compiling 1 java file(s)...done
jrake> unit_test
compiling 1 java file(s)...done
.
Time: 0.009

OK (1 test)

jrake> clean unit_test
deleting 10 file(s)...done
compiling 1 java file(s)...done
compiling 1 java file(s)...done
.
Time: 0.002

OK (1 test)

jrake> exit

You can download the entire sample project via subversion here:

svn://svn.foemmel.com/blog/jrake/testing

Just run the "build.bat" or "build.sh" scripts with no arguments to bring up the JRake Shell. If any arguments are passed in, the behavior is the same as before i.e. the targets will be built then the script will terminate.

Next Steps

Now we just need to get the app up and running...

No comments: