Taming the beast: Using JRuby and RSpec to test a J2EE application

Working with J2EE applications is something like wandering in a jungle: you never quite know what wild animal you’ll find around the next corner… it might be an ORM tool like Hibernate; it could be an application framework like Spring or Struts with lots of confusing XML files, or it might just be a long list of obscure JAR files that you need to find and download… whatever it is, you’re guaranteed to spend countless hours wasting time learning things you really didn’t want to know. This article will show how you can get your J2EE application under control by using JRuby and RSpec… but first, let’s take a quick look at what RSpec is and how it’s normally used with Ruby.

Using RSpec with Ruby

If you are a Ruby developer, you would probably write code similar to this to add up an array of numbers:

class Calculator
  def self.sum numbers
    numbers.inject do |sum, x|
      sum+x
    end
  end
end

This is simple enough to run:

$ irb
irb(main):001:0> require 'calculator.rb'
=> true
irb(main):002:0> Calculator.sum [2, 3]
=> 5

One of the best things about Ruby are all of the different testing tools available to you – for example, you could write a test for this method using RSpec like this:

require 'calculator.rb'
describe "Calculator" do
  it "should add numbers correctly" do
    Calculator.sum([1, 2]).should == 3
    Calculator.sum([2, 2]).should == 4
    Calculator.sum([2, 3, 4]).should == 9
  end
end

And then run the spec as follows:

$ spec calculator_spec.rb 
.
Finished in 0.001703 seconds
1 example, 0 failures

RSpec allows you to write test code that is readable, and also behavior-oriented; that is, the tests reflect the way an end user might actually behave. In fact, RSpec is really just the first step towards behavior-driven-development. The Ruby community also benefits from other tools such as WebRat, Cucumber, etc., that can make testing very easy and effective.

A J2EE sample app

Let’s rewrite the “sum” Ruby method above using Java. To make this feel more like an actual J2EE application, we’ll use a service class that will perform the actual sum operation for us, and set it up with the Spring framework. We can start by writing an XML file called “ApplicationContext.xml” for Spring to use, and declare a bean called “calculatorService:”

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
  "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
  <bean id="calculatorService"
    class="calculator.CalculatorServiceImpl">
  </bean>
</beans>

In a real J2EE app, we would probably have an interface called “CalculatorService” like this:

package calculator;
public interface CalculatorService {
  public int sum(int[] array);
}

And then we’d implement it using a concrete class, like this:

package calculator;
public class CalculatorServiceImpl implements CalculatorService {
  public int sum(int[] array)
  {
    int sum = 0;
    for (int number: array)
    {
      sum += number;
    }
    return sum;
  }
}

And finally, let’s write a simple Java command line client for this so we can test running it from the command line:

package calculator;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class CalculatorApp {
  public static void main(String[] args) throws Exception {
    int[] array = { 2, 3 };
    ClassPathXmlApplicationContext application_context =
      new ClassPathXmlApplicationContext("ApplicationContext.xml");
    CalculatorService calculator =
      (CalculatorService)application_context.getBean("calculatorService");
    System.out.println("2 + 3 is: " + Integer.toString(calculator.sum(array)));
  }
}

This will also help us figure out how to write the ruby spec later. If you run this from Eclipse or your favorite Java IDE, you’ll get:

Jun 25, 2009 12:21:58 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3479e304: display name [org.springframework.context.support.ClassPathXmlApplicationContext@3479e304]; startup date [Thu Jun 25 12:21:58 EDT 2009]; root of context hierarchy
Jun 25, 2009 12:21:58 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [ApplicationContext.xml]
Jun 25, 2009 12:21:58 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@3479e304]: org.springframework.beans.factory.support.DefaultListableBeanFactory@604788d5
Jun 25, 2009 12:21:58 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@604788d5: defining beans [calculatorService]; root of factory hierarchy
2 + 3 is: 5

This is starting to feel like an actual “Enterprise” J2EE application now that we have an XML config file and lots of confusing information being logged!

Running RSpec with JRuby

Now that we have a J2EE application to test, let’s get started with JRuby. First, you will need to download and install JRuby. This is really just a matter of downloading the TAR file and then placing the JRuby bin folder on your path. Test that you have it setup properly by running this command:

$ jruby --version
jruby 1.1.5 (ruby 1.8.6 patchlevel 114) (2008-11-03 rev 7996) [x86_64-java]

Now let’s update our Ruby spec from above and get it to work with Java. Here’s what we had for Ruby:

require 'calculator.rb'
describe "Calculator" do
  it "should add numbers correctly" do
    Calculator.add([1, 2]).should == 3
    Calculator.add([2, 2]).should == 4
    Calculator.add([2, 3, 4]).should == 9
  end
end

The key to using JRuby to test Java is to add this line at the top of the spec:

require 'java'

This tells JRuby that we want to allow the Ruby code to call Java directly. But what code should we try to call? The nice thing about the Spring framework is that is makes a series of “beans,” i.e. Java objects, available to us. The reason I went to the trouble of adding the Spring framework to this sample app is that for a real J2EE application using Spring to create and load a Java object will be the simplest path towards testing your target application. What I did while testing a real J2EE application was to:

  • Identify what business logic I wanted to test
  • Look for the Java object that was the top-level, simplest interface to that business logic under the user interface layer
  • Find a bean in the ApplicationContext.xml file that corresponded to that Java object. In my case, there actually was no such bean, and I had to slightly modify the application I was testing by adding a new <bean> tag to one of the Spring XML files.

If your application is not using Spring, you might simply be able to create a Java object directly from your Ruby code, or in the worst case scenario you might have to make Java code changes to the target J2EE app to break dependencies that the object you’d like to test has on other objects, interfaces, services, etc., allowing you to create it in isolation. Michael Feathers has written an entire book on dependency breaking techniques.

Back to this sample app: here in our Ruby spec we can follow the same pattern that I used above in the command line Java client: calling “ClassPathXmlApplicationContext” to get the application context, and then creating a bean with getBean. The other thing we need to add are “include_class” directives that indicate to JRuby which Java classes should be loaded from the classpath and made available to the Ruby script. Here’s the spec code with the new JRuby code changes in bold:

require 'java'
include_class 'org.springframework.context.ApplicationContext'
include_class
  'org.springframework.context.support.ClassPathXmlApplicationContext'
describe "Calculator" do
  it "should add numbers correctly" do
    application_context =
      ClassPathXmlApplicationContext.new "ApplicationContext.xml"
    calculator =  application_context.getBean "calculatorService"
    calculator.sum([1, 2]).should == 3
    calculator.sum([2, 2]).should == 4
    calculator.sum([2, 3, 4]).should == 9
  end
end

Now let’s try to run it using JRuby. Another trick with JRuby is knowing how to use the “-S” option – this allows you to run a Ruby command like “gem” or “spec,” but inside a JRuby session. So here’s how to run our new spec using JRuby:

$ jruby -S spec calculator_spec.rb 
(eval):1:in `include_class': cannot load Java class
  org.springframework.context.ApplicationContext (NameError)
  from /Users/pat/src/jruby-1.1.5/lib/ruby/site_ruby/1.8/builtin/javasupport/core_ext/object.rb:38:in `eval'
  from /Users/pat/src/jruby-1.1.5/lib/ruby/site_ruby/1.8/builtin/javasupport/core_ext/object.rb:67:in `include_class'
  from /Users/pat/src/jruby-1.1.5/lib/ruby/site_ruby/1.8/builtin/javasupport/core_ext/object.rb:38:in `each'
  from /Users/pat/src/jruby-1.1.5/lib/ruby/site_ruby/1.8/builtin/javasupport/core_ext/object.rb:38:in `include_class'
  from calculator_spec.rb:2

So what is the error message all about? This just means that JRuby wasn’t able to find the ApplicationContext class from Spring on the classpath. But what is the classpath anyway? It would be nice to be able to simply specify the classpath using a command line option, the way you do with a java command line using “-cp” for example. But for JRuby you need to specify the classpath as an environment setting. To make this easier, I wrote a simple shell script for running a JRuby spec:

BASE=`pwd`
CLASSPATH=$BASE/lib/spring-2.5.1.jar
export CLASSPATH
jruby -S spec $1

This just gets the current working directory and constructs the classpath setting, indicating where to find the Spring JAR file. Finally we export the classpath value and call JRuby. This will work on the Mac and Linux; for Windows you would need something slightly different. Anyway, now I can run my specs like this:

$ ./jruby-spec.sh calculator_spec.rb 
F
1)
NativeException in 'Calculator should add numbers correctly'
java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory
org/springframework/util/ClassUtils.java:73:in `<clinit>'
org/springframework/core/io/DefaultResourceLoader.java:52:in `<init>'
org/springframework/context/support/AbstractApplicationContext.java:198:in `<init>'
org/springframework/context/support/AbstractRefreshableApplicationContext.java:80:in `<init>'
org/springframework/context/support/AbstractXmlApplicationContext.java:58:in `<init>'
org/springframework/context/support/ClassPathXmlApplicationContext.java:119:in `<init>'
org/springframework/context/support/ClassPathXmlApplicationContext.java:66:in `<init>'
sun/reflect/NativeConstructorAccessorImpl.java:-2:in `newInstance0'
sun/reflect/NativeConstructorAccessorImpl.java:39:in `newInstance'
sun/reflect/DelegatingConstructorAccessorImpl.java:27:in `newInstance'
java/lang/reflect/Constructor.java:513:in `newInstance'
org/jruby/javasupport/JavaConstructor.java:226:in `new_instance'
org/jruby/java/invokers/ConstructorInvoker.java:100:in `call'
org/jruby/java/invokers/ConstructorInvoker.java:180:in `call'
etc...

Oops – it turns out that Spring actually requires the Apache Commons logging JAR file to be on the classpath also. This sample app is definitely reminding me of a complex J2EE app running on WebSphere or WebLogic! Let’s add commons-logging-1.0.4.jar to the classpath in my shell script and try again:

$ ./jruby-spec.sh calculator_spec.rb 
Jun 25, 2009 1:58:15 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4c6c3e: display name [org.springframework.context.support.ClassPathXmlApplicationContext@4c6c3e]; startup date [Thu Jun 25 13:58:15 EDT 2009]; root of context hierarchy
Jun 25, 2009 1:58:15 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [ApplicationContext.xml]
F
1)
NativeException in 'Calculator should add numbers correctly'
org.springframework.beans.factory.BeanDefinitionStoreException:
  IOException parsing XML document from class path resource
  [ApplicationContext.xml]; nested exception is java.io.FileNotFoundException:
  class path resource [ApplicationContext.xml] cannot be opened because
  it does not exist
org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java:334:in `loadBeanDefinitions'
org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java:295:in `loadBeanDefinitions'

Once again, I’ve forgotten something in my class path: this time it’s the ApplicationContext.xml file which I passed into ClassPathXmlApplicationContext in the spec. As the name implies, I need to put the XML file on the classpath in order for Spring to be able to find it. Let’s cut to the chase and put everything I need onto the classpath… here’s the final version of jruby-spec.sh (the classpath line split into two for readability):

BASE=`pwd`
CLASSPATH=$BASE/bin:$BASE/resources:$BASE/lib/spring-2.5.1.jar:
  $BASE/lib/commons-logging-1.0.4.jar
export CLASSPATH
jruby -S spec $1

Now JRuby will be able to find everything that it needs: Spring, Apache Common Logging, the ApplicationContext.xml file, and also the application’s class files saved under “bin” by Eclipse. Now if I run the spec once more it should all work:

$ ./jruby-spec.sh calculator_spec.rb 
Jun 25, 2009 2:02:22 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1717d968: display name [org.springframework.context.support.ClassPathXmlApplicationContext@1717d968]; startup date [Thu Jun 25 14:02:22 EDT 2009]; root of context hierarchy
Jun 25, 2009 2:02:22 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [ApplicationContext.xml]
Jun 25, 2009 2:02:22 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@1717d968]: org.springframework.beans.factory.support.DefaultListableBeanFactory@5a21fdc8
Jun 25, 2009 2:02:22 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@5a21fdc8: defining beans [calculatorService]; root of factory hierarchy
F
1)
TypeError in 'Calculator should add numbers correctly'
for method sum expected [[I]; got: [org.jruby.RubyArray];
  error: argument type mismatch calculator_spec.rb:8:
Finished in 0.70559 seconds
1 example, 1 failure

What’s this error all about? I do have the correct classpath now, but what does “RubyArray” mean? I thought I passed an array of integers into the calculator service:

calculator.sum([1, 2]).should == 3

Well it turns out that Ruby and JRuby aren’t quite the same thing. Since JRuby is actually a Java application itself, it implements each Ruby class with a corresponding Java class. For the Ruby Array class, JRuby has created a class called “org.jruby.RubyArray.” When you call Java code and pass in Ruby objects, JRuby actually provides the Java equivalent of these Ruby objects to the Java code. That’s why we get an error here; our calculator service doesn’t expect a RubyArray – it expects int[] instead.

To avoid this error, we need to convert the RubyArray into a normal Java array, using a JRuby method called “to_java()”, like this:

calculator.sum([1, 2].to_java(:int)).should == 3

To_java takes a symbol as a parameter, which indicates what type each element of the Ruby array should be converted into. Now our spec will pass!

$ ./jruby-spec.sh calculator_spec.rb 
Jun 25, 2009 2:15:37 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1717d968: display name [org.springframework.context.support.ClassPathXmlApplicationContext@1717d968]; startup date [Thu Jun 25 14:15:37 EDT 2009]; root of context hierarchy
Jun 25, 2009 2:15:37 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [ApplicationContext.xml]
Jun 25, 2009 2:15:37 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@1717d968]: org.springframework.beans.factory.support.DefaultListableBeanFactory@5a21fdc8
Jun 25, 2009 2:15:37 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@5a21fdc8: defining beans [calculatorService]; root of factory hierarchy
.
Finished in 0.28237 seconds
1 example, 0 failures

Taking a step back, I think what we’ve done is quite interesting; we have:

  • Pulled a J2EE application out of Eclipse, IntelliJ or whatever IDE you project uses, and started to run it from the command line instead. For me, avoiding a confusing and complex IDE makes the application easier to work with… more like a Rails app.
  • Documented what the classpath needs to be and what JAR files the application requires in an understandable text file format, rather than having it hidden away in a confusing Eclipse dialog box.
  • Exposed the J2EE application’s business logic to testing below the user interface layer – nothing here involves a web browser or user interface testing tool like Selenium.
  • Started to apply behavior driven development to a J2EE application, using the best tools available, which happen to be written in Ruby.
In a future post, I’ll try to take the next step of using Cucumber features to test this same J2EE sample app. It will be interesting to find out if Cucumber works will with JRuby.