Chris chided me in the Java only gets 10mpg comments for not giving Groovy or Rhino a go. Chris has a fondness for busting my chops equaled by few. However, he does it because he cares, so I thought a bit about using non-Java languages on the JVM.
I still think that in general, I have little use for the Java platform. If I want to write code in something like Ruby or Python, I’ll just use Ruby or Python or whatever as they’re originally distributed. Bolting them on to the JVM really doesn’t do anything for me. That said, if I can use them to get out of ugly, tedious work, maybe they’re worth my time.
As it happens, I recently inherited about a quarter million lines of Java code. There are chunks of it that are somewhat lacking in terms of unit tests, and I’m one of those mutants who really doesn’t like to fix code without exerciseing the problem with a test first. With this project, I’ve been having to write hundreds and hundreds of lines of test code to make very simple changes. It’s safe, but it’s not exactly efficient or fun.
So I took Chris up on his suggestion and gave JRuby a look for writing tests. It works surprisingly well. There’s not that much setup work that you need to do to get the environment up and running, and once you do it’s rather nice to not have to slog through the ugly world that is Java the language to get your tests written.
Here’s a very brief sample. What I wanted to do is have a very traditional ant-based project with the standard init->compile->test->dist dependency chain, but sub JUnit out for JRuby and Ruby’s unit test framework. First, the silly little Java class that I used for testing:
src/java/org/obfsucated/example/Overbearing.java
package org.obfuscated.example;
public class Overbearing {
/** Javaize a string.
**
** Everybody knows that Java people can't stand data unless
** it's XML or some sort of proprietary binary format that
** you have to pay Sun $10,000/seat to use. Our Overbearing
** class should be able to take any pedestrian string and
** turn it in to something that any Java programmer would
** love and cherish forever.
**
** @param original an ugly, non-Enterprise-compliant string.
** @returns a String that you could put in some sort of bean.
*/
public String javaize(String original) {
if(null == original)
return null;
StringBuilder sb = new StringBuilder();
sb.append("<java-is-rad>< ![CDATA[");
sb.append(original);
sb.append("]]></java-is-rad>");
return sb.toString();
}
}
And here are the relevant Ruby source files:
src/jruby/test_all.rb
require 'test_overbearing'
src/jruby/test_overbearing.rb
require 'test/unit'
require 'java'
Overbearing = org.obfuscated.example.Overbearing
class TestOverbearing < Test::Unit::TestCase
def setup
@overbearing = Overbearing.new
end
def teardown
@overbearing = nil
end
def test_creation
assert_not_nil @overbearing, "We should be able to create an Overbearing object"
end
def test_simple_string
original = "Hello World"
expected = "<java-is-rad><![CDATA[Hello World]]></java-is-rad>"
resultant = @overbearing.javaize(original)
assert_equal resultant, expected
end
def test_nil_string
original = nil
expected = nil
resultant = @overbearing.javaize(original)
assert_equal resultant, expected, "Nil strings should stay that way"
end
end
It’s all pretty staraightforward stuff. I like to run unit test suites by requiring all of my tests in one file and just calling that with the interpreter. (Thus the “test_all.rb” file.) There are without doubt better ways to do this. You get the general idea, though.
One gotcha is that if you have a package with calital letters, you can’t alias it as above. So let’s say you have a class with a fully-qualified name of org.obfuscated.BadPackageName.Whatever. JRuby — or Ruby itself, if I had to guess based on the way identifiers are usually handled — will not be able to find it if you just say
Whatever = org.obfuscated.BadPackageName.Whatever
Instead, use the include_class function to make sure it’s
loaded.
include_class('org.obfuscated.BadPackageName.Whatever')
Long story short, though: don’t use capital letters in package names.
The Ant setup isn’t too difficult, either. Assume the following properties are available:
- jruby.dir
- The directory where JRuby is installed
- jruby.test.dir
- The directory where you keep your test scripts (testall.rb and testoverbearing.rb in this case)
The relevant parts of the build.xml file look like this:
<path id="jruby.classpath">
<fileset dir="${jruby.dir}/lib">
<include name="*.jar"/>
</fileset>
</path>
<target name="test" depends="compile">
<java classname="org.jruby.Main" fork="true" failonerror="true">
<classpath refid="project.classpath"/>
<classpath refid="jruby.classpath"/>
<sysproperty key="jruby.base" value="${jruby.dir}"/>
<sysproperty key="jruby.home" value="${jruby.dir}"/>
<sysproperty key="jruby.script" value="jruby"/>
<sysproperty key="jruby.shell" value="/bin/sh"/>
<arg value="-I"/>
<arg value="${jruby.test.dir}"/>
<arg value="${jruby.test.dir}/test_all.rb"/>
</java>
</target>
Type “ant test” and away you go. Now you now longer have to slog through things with Java, and you’ve removed the compile phase from your test cycle. Bonus.
So thanks, Chris. That was a fine idea.