From the Agony of JUnit to the Ecstasy of RSpec
In the past I have had a love hate relationship with unit testing and testing tools in general. I have had a long, healthy, even dreamy relationship with test driven development though. Aahh how it improves design and prevents issues from reaching users!
I am often unhappy with the amount of effort it takes to write tests using some of the tools and libraries out there. I’ve been around the block from Perl to Lisp to Java, even C++ and C (I found using Chicken to be an easy way to write tests for C libraries, but I digress…) I have found, both with myself and with teams I’ve worked with, that if the effort of writing tests is high enough, less (or no) tests will be written. This is partly because test themselves do not represent business value or end-user features, so most things you can do to reduce the effort of writing or running tests will cause more tests to be written. This includes not just the boilerplate set up and typing you have to do, but the amount of time it takes to run your tests (long running tests unfortunately re-introduce the flow destroying edit-compile-debug cycle back into modern dynamic application development). Reducing effort is, of course, dependent on ROI, which is different for each person and team.
A good friend of mine, Jon Tran, pushes the idea that if you can make something abundant (nearly free) you haven’t just made something better, you’ve made something entirely different. I love this idea and try to use it to guide process and tool improvements for myself and my team.
When I started working with Ruby on Rails I found testing to be so well integrated into how you worked on your Rails project that it was, in fact, abundant. It was so much easier to test your code that writing tests was no longer a chore, but it fit easily, even joyfully into the work-flow. Working more recently with a Rails expert, Trotter Cashion, I was introduced to RSpec. Our team was also blessed one fine day by a visit from Kevin Fitzpatrick who bootstrapped our use of Cucumber in a single afternoon pairing session.
These two tools were pure love. They gave me the same kind of feelings that declarative and functional programming give me – you say what you want it to be, not just how you want it to get there. RSpec made unit testing even more abundant, Cucumber made front end or integration testing abundant. Those two tools went a long way to helping our team write more tests and more effective tests, improving the design of our code, applications and ultimately reducing the number of issues (especially regressions) that made it both to and past our excellent QA testers (we’re only human after all).
Writing unit tests for the Java portions of our systems I have really started to miss the clarity and abundance that RSpec provided. Then I stumbled across the maven-rspec-plugin which uses JRuby to execute rspec base tests during the test phase of the maven life cycle.
The storm clouds have parted and the sun is shining again. This is exactly what I was looking for; I just hadn’t known it until I found it.
We’re already using JRuby within the project and have been happy with the access to all of the JVM based libraries it provides to us (not to mention easy integration with our Clojure code, but that’s another post altogether). The tools are a great fit for our team, we’re already familiar with Ruby, RSpec and writing those kinds of tests – just not yet in our Java portions of the application.
On your mark, get set…I followed the instructions in that post, but ended up with a NullPointerException when the plugin tried to write out the runner shell script. I checked out the code from the CodeHaus svn repository and poked around until I found a work around, it seems that the plugin requires a systemProperties
tag in its configuration, even if it’s not used, to prevent the NPE
. It just has to be present with at least one property present.
Rob DiMarco discovered that the plugin, rspec or something about this confluence of tools and libraries doesn’t work on JRuby 1.4 (see the stacktrace for more information), so make sure you download and install JRuby 1.3.1 for now.
To get started you’ll need JRuby installed, make sure you get 1.3.1 and you’ll have to set the JRUBY_HOME
environment variable to point to your installation. You’ll need maven of course as well.
The plugin configuration we were able to get working is the pom.xml
fragment you see here:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>rspec-maven-plugin</artifactId> <configuration> <jrubyHome>${env.JRUBY_HOME}</jrubyHome> <sourceDirectory>${basedir}/src/test/specs</sourceDirectory> <outputDirectory>${basedir}/target</outputDirectory> <systemProperties> <property><name>testProp</name><value>testValue</value></property> </systemProperties> </configuration> <executions> <execution> <id>test</id> <phase>test</phase> <goals> <goal>spec</goal> </goals> </execution> </executions> </plugin>
This configures rspec to be run during the test phase of the maven build. It also configures the plugin to look for spec tests within the src/test/specs directory. When first executed, the plugin will create two helper scripts in the project’s target/
directory: rspec-runner.rb
and run-rspecs.sh
. The run-rspecs.sh
can be used to run the tests more quickly and succinctly from the command line, as opposed to relying on mvn test
to execute the tests. The runner shell script passes its arguments through to rspec, so you can use all the features the rspec runner supports, like running a single spec file or a single test within a spec (by line number or name).
Example Maven Project
I’ve created an example Maven project in my github sandbox that you should be able to use as a reference or starting point for your own project. If it doesn’t work for you, after following the instructions, please let me know and I’ll try to work through any issues with you.
The sample project includes a single java class (SomeClass
) and a single spec test which exercises it. It is meant as a minimal working example at the moment, as I explore using RSpec for our Java testing I’ll try to keep it up to date with any new findings (and I hope to figure out how to get it working with JRuby 1.4).
The Java code is a simple class with a few members including a method to fetch a url:
package com.github.kyleburton; import org.apache.commons.io.IOUtils; import java.net.*; import java.io.*; public class SomeClass { private String _userName; private String _remoteUrl; private String _theContent = null; private Downloader _downloader; public SomeClass() { _userName = "*a default*"; _remoteUrl = "http://localhost/"; _downloader = new Downloader(_remoteUrl); } public SomeClass(String name, String url) { _userName = name; _remoteUrl = url; _downloader = new Downloader(url); } public String getContent() throws IOException { if ( null == _theContent ) _theContent = _downloader.download(); return _theContent; } public String getUserName() { return _userName; } public void setUserName(String userName) { _userName = userName; } public String getRemoteUrl() { return _remoteUrl; } public void setRemoteUrl(String remoteUrl) { _remoteUrl = remoteUrl; _downloader = new Downloader(_remoteUrl); } public void setDownloader(Downloader downloader) { _downloader = downloader; } public static class Downloader { private String url; public Downloader(String url) { this.url = url; } public String download() throws IOException { return IOUtils.toString(new URL(url).openStream()); } } }
The rspec test exercises the class, including using Mockito to ensure that getContent
actually delegates to the Downloader
class to fetch the remote data (without it actually going out and hitting a website).
import com.github.kyleburton.SomeClass import org.mockito.Mockito describe SomeClass do before(:each) do @the_name = "a user" @the_url = "http://asymmetrical-view.com/" @some_class = SomeClass.new @the_name, @the_url end it "should accept constructor parameters" do @some_class.user_name.should == @the_name @some_class.remote_url.should == @the_url end it "should download the conten when content is accessedt" do some_content = "this is some content" downloader = Mockito.mock(SomeClass::Downloader.java_class) Mockito.when(downloader.download()).then_return(some_content) @some_class.downloader = downloader @some_class.content.should_not be_empty @some_class.content.should == some_content end end
Finally here is an exmaple of how you can run irb
to get a Ruby REPL for your project with all of your project dependencies on the classpath and available in irb
(assuming you’ve compiled it with maven first):
# run-jirb.sh CLASSPATH="target/test-classes:target/classes" CLASSPATH="$CLASSPATH:`mvn -Dmdep.outputFile=/dev/stderr dependency:build-classpath 2>&1 > /dev/null`" export CLASSPATH jirb "$@"
Conclusion
Being able to use RSpec to test our Java and Clojure code-bases is going to help our team be more productive, it will bring the same sense of fun to working on these parts of our system – reduce the effort it takes and leading to more tests. I am quite satisfied with the improvement in morale it has brought to the team when we’re working in these areas just in the first few days we’ve had it.
Special Thanks
Special thanks to Jon Tran for reading drafts of this post.
Image and Photo Credits
- “Duke” Pictures Courtesy of Project Kenai
- Diagrams