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 int ONE_SECOND = 1000;
-
(60 * 60) + (23 * 60) + 45);
-
-
private AbstractPiemanLog log;
-
-
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() {
-
}
-
-
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());
-
}
-
-
* 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.
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!
Charles Oliver Nutter
21 Dec 06 at 3:53 pm
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
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
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
Thanks for the link Joakim, that looks like pretty cool stuff.
Kerry
10 Sep 07 at 3:55 pm
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