Kerry Buckley

What’s the simplest thing that could possibly go wrong?

Testing java code using rspec and jruby

6 comments

Now that rspec runs under jruby, and with a few hours to spare, I thought I’d have a play.

[Update:] I’ve simplified the import of java.util.Date following a suggestion in a comment (from Charles Oliver Nutter, no less!). I also noticed that I wasn’t using rspec’s setup properly (or at all), so I’ve tweaked the samples a little. I haven’t got the code here to try out, so there may be typos.

Getting rspec installed in the jruby environment was a simple case of downloading the gem and installing it using the jruby gem command. This installs a spec command in $JRUBY_HOME/bin, but no corresponding spec.bat wrapper. If you’re unfortunate enough to be using Windows (as I was, being at work), it’s trivial to copy one of the existing wrappers and change the command it runs. I had to use the wrapper script to run the specs from ant, because I couldn’t get it to work by using org.jruby.Main directly (probably just user error – I didn’t bother investigating much as I was only really interested in the specs themselves).

To get a direct comparison, I concentrated on reproducing an existing junit test, rather than trying to use rspec to do some actual TDD/BDD. I suspect this was slightly harder than it would have been to have used rspec while I was writing the class in the first place, but was still surprisingly painless (particularly as my ruby’s a bit rusty).

Comparisons between equivalent junit and rspec code

Here are a few ‘compare and contrast’ excerpts from the two files. I’ve removed a few bits and pieces that weren’t really relevant to the examples, and changed a few minor things round in the java to make for a better comparison.

Preamble
package com.bt.csam.pieman.domain;

import java.util.Date;
import junit.framework.TestCase;
import com.bt.csam.test.TestUtils;

public class AbstractPiemanLogTests extends TestCase {
require 'java'
include_class 'com.bt.csam.pieman.domain.TestPiemanLog'
include_class 'com.bt.csam.pieman.domain.Status'
include_class 'com.bt.csam.test.TestUtils'

include_class(”java.util.Date”) { “JDate” }

Nothing particularly noteworthy there. There are a couple of extra imports in the ruby version which weren’t required in Java because the test class was in the com.bt.csam.pieman.domain package. If there had been more, it would probably have made more sense to just do include_package 'com.bt.csam.pieman.domain' and grab the lot.

The import of java.util.Date has been aliased to JDate, because it would otherwise clash with Ruby’s Date class.

Constants and setup
    private static final Long USER = "foo";
    private static final Long FIVE_MINUTES = new Long(5 * 60);
    private static final Long TEN_MINUTES = new Long(10 * 60);
    private static final int ONE_SECOND = 1000;
    private static final Long ONE_HOUR_23_MINUTES_45_SECONDS = new Long(
            (60 * 60) + (23 * 60) + 45);

    private AbstractPiemanLog log;

    protected void setUp() throws Exception {
        TestUtils.setSecurityContext(USER);
        log = new TestPiemanLog();
    }
FIVE_MINUTES = 5 * 60
TEN_MINUTES = 10 * 60
FIFTEEN_MINUTES = 15 * 60
ONE_HOUR_23_MINUTES_45_SECONDS = (60 * 60) + (23 * 60) + 45
ONE_SECOND = 1000
USER = 'foo'
TestUtils.setSecurityContext USER

Again, very similar. I chose to declare the log object locally to each context in the ruby version, which is why it’s missing from the global setup.

The first test/spec
    public void testCreatedBySetOnCreate() {
        assertEquals(USER, log.getCreatedBy());
    }
context "A newly-created log" do
  setup do
     @log = TestPiemanLog.new
  end
  
  specify "should be marked as created by current user" do
    @log.createdBy.should == USER
  end
end

That was painless (once I’d figured out whether to use should.be, should.eql, should.equal or should ==). My first attempt used @log.getCreatedBy, but I wondered whether it would call the getter automatically, and sure enough it did.

Working with dates, collections and booleans

The date stuff would all have been a lot easier if the class had used an external mockable time source, which is a technique suggested in Mock roles, not objects [PDF]. This is one of those things that seems obvious once you’ve seen it (which is always after you’ve spent ages working around the same problem the hard way).

Anyway, reproducing the existing tests:

    public void testTimestampSetOnCreate() {
        assertTrue((new Date().getTime() - log.getTimestamp().getTime()) < ONE_SECOND);
    }

    public void testCurrentVersionOnNewObject() {
        assertTrue(log.isCurrentVersion());
    }

    // ...

    public void testDealingClockNotRunningAfterStopDealingClock() {
        log.stopDealingClock();
        assertFalse(log.getDealingClockRunning().booleanValue());
    }

    public void testStoredDealingTimeSetToSecondsSinceLatestDealingStartTimeWhenDealingClockStoppedForTheFirstTime() {
        // force the start time to five minutes ago
        log.setLatestDealingStartTime(fiveMinutesAgo());
        log.stopDealingClock();
        assertEquals(FIVE_MINUTES, log.getStoredDealingTime());
    }

    private Date fiveMinutesAgo() {
        return new Date(new Date().getTime() - FIVE_MINUTES.longValue()
                * ONE_SECOND);
    }
def five_minutes_ago
  JDate.new(JDate.new.getTime - FIVE_MINUTES * ONE_SECOND)
end

# ...

context "A newly-created log" do
  setup do
    @log = TestPiemanLog.new
    @now = JDate.new.getTime
  end

  # ...
  specify "should be timestamped with the creation time" do
    # this would be easier if the class under test had a time provider we could mock
    @log.timestamp.getTime.should_be_close now, ONE_SECOND
  end
  
  specify "should have no title prefixes" do
    @log.titlePrefixes.should.be.empty
  end
  
  specify "should be the current version" do
    log.should.be.currentVersion
  end
  # ...
end

context "A log that has the dealing clock stopped after five minutes" do
  setup do
    @log = TestPiemanLog.new
    # pretend it started five minutes ago.
    @log.latestDealingStartTime = five_minutes_ago
    @log.stopDealingClock
  end

  specify "should not have the dealing clock running" do
    @log.should.not.be.dealingClockRunning
  end
  
  specify "should have a stored dealing time of five minutes" do
    @log.storedDealingTime.should == FIVE_MINUTES
  end
end

I expect there's a better way than faffing about with java Date objects, but it's not too bad as it is. Note that setLatestDealingStartTime is called automatically, the same as the getters.

For boolean values, the behind-the-scenes magic works again, and @log.should.be.currentVersion acts like assertTrue(log.isCurrentVersion()) (only more readable). Once again, ruby passes Arthur C Clarke's sufficiently advanced technology test.

Finally, the rspec should.be.empty (and similar constructs) all work seamlessly with java collections too.

Human-readable spec output

Here's the output from running the original junit report through a stylesheet:

Abstract pieman log

  • created by set on create
  • modified by by set on create
  • timestamp set on create
  • get title prefixes
  • current version on new object
  • status defaults to open
  • newly created log has latest dealing start time equal to create time
  • newly created log has dealing clock running
  • newly created log has zero stored dealing time
  • dealing clock not running after stop dealing clock
  • dealing clock running after start dealing clock
  • stored dealing time set to seconds since latest dealing start time when dealing clock stopped for the first time
  • latest dealing start time set to current time when dealing clock started
  • latest dealing start time set to null when dealing clock stopped
  • dealing time formatted as colon separated hours minutes and seconds
  • seconds since latest dealing start time added to stored dealing time when dealing clock stopped after the first time
  • stopping clock when already stopped has no effect on dealing time fields
  • starting clock when already started has no effect on dealing time fields
  • dealing clock stopped when log is closed
  • dealing time equals stored dealing time when clock stopped
  • dealing time equals stored dealing time plus time since last start when clock running
  • cleared time always equals closed date

And here's the rspec equivalent:

A newly-created log
- should be timestamped with the creation time
- should be marked as created by current user
- should be marked as modified by current user
- should have no title prefixes
- should be the current version
- should be open
- should have the dealing clock running
- should have the latest dealing clock start time set to its timestamp
- should have no stored dealing time
A log after the dealing clock has been started
- should have the dealing clock running
A log that has the dealing clock stopped after five minutes
- should not have the dealing clock running
- should have a stored dealing time of five minutes
- should have a latest dealing clock start time of null
- should ignore further attempts to stop the clock
A log that has the dealing clock stopped after five minutes and already has a stored dealing time of ten minutes
- should have a stored dealing time of fifteen minutes
- should display dealing time as 0:15:00
A log that has the dealing clock stopped after five minutes then restarted
- should have a latest dealing clock start time of the current time
- should ignore further attempts to start the clock
A log that has had the dealing clock running for five minutes and has a stored dealing time of ten minutes
- should display dealing time as 0:15:00 (ish)
A log with the clock stopped and a stored dealing time of one hour 23 minutes 45 seconds
- should display dealing time as 1:23:45
A closed log
- should not have the dealing clock running
- should have a cleared time matching the closed date

I don't think there can be much debate about which is more readable.

Where's the when?

A couple of months ago, Dan North made this comment on the subject of rspec:

I’m a big fan of Ruby, and I like some of the ideas behind rspec. However I’ve been working with Niclas Nilsson on a Ruby port of JBehave called RBehave, which we think conveys the intent of BDD more clearly.

At the time I wasn't sure quite what he meant, but having been to the Awesome acceptance testing session that Dan ran with Joe Walnes at XP Day 06, I realised how useful the given...when...then approach to behaviour specification is. Unfortunately rspec only has two levels – context and specify.

One of my specs (in English: when a log is closed, the clock should be stopped) ended up like this:

context "A closed log" do
  setup do
    @log = TestPiemanLog.new
    @log.status = Status::CLOSED
    now = JDate.new
    @log.closedDate = now
  end

  specify "should not have the dealing clock running" do
    @log.should.not.be.dealingClockRunning
  end
  
  specify "should have a cleared time matching the closed date" do
    @log.clearedTime.should == @log.closedDate
  end
end

That's effectively moving the when into the given. The alternative would have been to move it into the then, but then it ends up duplicated in two specifications :

def close_log(log)
  log.status = Status::CLOSED
  now = JDate.new
  log.closedDate = now
end

context "An open log" do
  setup do
    @log = TestPiemanLog.new
  end

  specify "should not have the dealing clock running after being closed" do
    close_log(@log)
    @log.should.not.be.dealingClockRunning
  end
  
  specify "should have a cleared time matching the closed date after being closed" do
    close_log(@log)
    @log.clearedTime.should == @log.closedDate
  end
end

Neither of these seems quite right. It would be nice if you could have one or more events per context, with one or more specs per event. The above would then become something like this (I've added an after rather than going with given...when...then, partly because when and then are reserved words):

context "An open log" do
  setup do
    @log = TestPiemanLog.new
  end

  after "the log is closed" do
    setup do
      @log.status = Status::CLOSED
      now = JDate.new
      @log.closedDate = now
    end

    specify "the dealing clock should not be running" do
      @log.should.not.be.dealingClockRunning
    end
    
    specify "the cleared time should match the closed date" do
      @log.clearedTime.should == @log.closedDate
    end
  end
end

Of course, I've only dipped a toe into rspec, so maybe it supports that kind of structure already.

Technorati Tags: , , , ,

Written by Kerry

December 21st, 2006 at 1:44 pm

Posted in Agile,Java,Ruby

6 Responses to 'Testing java code using rspec and jruby'

Subscribe to comments with RSS or TrackBack to 'Testing java code using rspec and jruby'.

  1. A very interesting post! I’m excited to see RSpec start to get some attention for testing Java code; it’s really a great tool and runs almost flawlessly under JRuby (minus a few remaining RSpec specs that appear to be text formatting issues).

    One suggestion: You can also use include_class with classes that might conflict as follows, to eliminate the Java::Date and module Java stuff:

    include_class(“java.util.Date”) { “JDate” }
    date = JDate.new

    It’s quite a bit more readable.

    Thanks for this post; I’ll probably be blogging it soon!

  2. I hope this is just the beginning of rspec-java descriptive posts. I believe one of the first questions from readers should be:
    ” What about tools? Junit is well integrated with Eclipse and all other IDEs. What is the fate of rSpec? ”

    If there was some way to have the rspec code somehow emulate what JUnit does, then that could solve this question – but since I am far from being a java programmer, I have no idea what else can be done on this matter.

    Evgeny

    30 May 07 at 12:13 pm

  3. Tool support is a good point (I’m programming in Ruby these days, and even without Java the IDE support isn’t great).

    I haven’t tried it yet (coincidentally I was just downloading it when you posted the comment!), but the upcoming NetBeans Ruby tools appear to integrate with RSpec. Whether you can jump to the declaration of a Java class/method from JRuby code I’m not sure, but it wouldn’t surprise me.

    Kerry

    30 May 07 at 12:25 pm

  4. I just blogged about integrating RSpec and JUnit 4.4. You might find it interesting: http://johlrogge.wordpress.com/2007/09/08/hybrid-theory/

    This way I can run my RSpec-tests as ai would run the if they where JUnit tests from eclipse and I don’t see why it wouldn’t work from idea, net-beans, ant, maven etc although I haven’t tested it yet.

    Joakim Ohlrogge

    9 Sep 07 at 9:31 am

  5. Thanks for the link Joakim, that looks like pretty cool stuff.

    Kerry

    10 Sep 07 at 3:55 pm

  6. You’re welcome :)

    I guess its about time to wrap it up and create some open source project from that so it can be used and developed further.

    Just need to find the time…

    Joakim Ohlrogge

    20 Sep 07 at 9:18 am

Leave a Reply

You must be logged in to post a comment.