Making FitNesse Maven friendly (now with Slim)
Update Thanks to Alan Palmer for this trick that makes this whole post unnecessary: If you use !path {java.class.path} You get the classpath that fitnesse was called with, without the need for changing the fitnesse code.
Regular readers of my blog may remember that I’ve researched how to get the classpath from the FitNesse-process inherited by the Fit process that FitNesse spawns when it runs a test. This trick is an easy way to get around having to specify classpath variables in your FitNesse tests. This blogposts provides an easier way, plus compatibility with the new Slim testrunner in FitNesse.
This is the basic process:
- Create a Maven project with a dependency on your System Under Test in addition to FitNesse.
- Insert a few bootstrap files and classes into the project (to be explained)
- Start
fitnesse.FitNesse
as a main class in the project - All classes in the project and its dependencies will now be available in Fixture classes
This article uses the 20081115 version of FitNesse, which is not yet in the Maven repository. To get it yourself, download it from the FitNesse website.
The problem
Fixing the classpath is exceedingly simple. But you need to understand a bit of the inner workings of FitNesse to get it to work. When you press the “test†button on a test, or the suite button on a test suite, FitNesse instantiates a class based on the registered “responderâ€. Our first order of business is to override this, but in order to do that, we need to take control over FitNesse.
You can do this by adding fitnesse.jar and fitlibrary.jar to your classpath, either manually in Eclipse, or by using a Maven dependency on org.fitnesse:fitnesse and org.fitnesse:fitlibrary.
Once FitNesse is in you IDE’s classpath, you can run the Java class “fitnesse.FitNesseâ€. This starts FitNesse on port 80. Navigate to, say “http://localhost/MyFirstTestâ€, and you’re ready to add a test.
In order to get it to work, though, you will have to do something like the following:
!path target/classes
!path /.m2/repository/org/fitnesse/fitnesse/20060719/fitnesse-20060719.jar
!path /.m2/repository/org/fitnesse/fitlibrary/20060719/fitlibrary-20060719.jar
!|my.test.ExampleFixture|
|first|second|sum?|product?|
|10|10|20|100|
|1|0|1|0|
|10000|1|10001|10000|
Ugh! Bad FitNesse!
The solution
What actually happens when you press “test” is that FitNesse creates and executes a Responder
class. You can override a responder by adding a file called plugins.properties
to the current working directory. Here is an example of such a file:
Responders = test:com.brodwall.fitnesse.InheritClasspathTestResponder
As you can see, I replace the test-responder with my own subclass. Here is the code of the InheritClasspathTestResponder
for the very latest version of FitNesse:
public class InheritClasspathTestResponder extends TestResponder {
private static final String PATH\_SEPARATOR = System.getProperty("path.separator");
/** For FitNesse 20081115 and later */
@Override
protected String buildClassPath() throws Exception {
return super.buildClassPath() + PATH\_SEPARATOR + getInheritedClassPath();
}
protected String getInheritedClassPath() {
String inheritedClasspath = "";
String parentClassPath = System.getProperty("java.class.path");
String[] classPathElements = parentClassPath.split(PATH\_SEPARATOR);
for (String element : classPathElements) {
inheritedClasspath += PATH\_SEPARATOR + "\\"" + element + "\\"";
}
return inheritedClasspath;
}
}
For older versions of FitNesse, you can instead override the buildCommand
method as follows:
/** For FitNesse 20080812 and earlier */
protected String buildCommand(PageData data, String program,
String classPath) throws Exception {
return super.buildCommand(data, program, classPath + getInheritedClassPath());
}
This works both with both the original Fit test runner and the new Slim test runner. To try it out with a Slim runner, insert the following into a test page:
!define TEST_SYSTEM {slim}
(Notice that Uncle Bob’s article introducting Slim mistakenly !defines TEST_RUNNER. The correct variable is TEST_SYSTEM)
Debugging
In my original experiment, I also managed to get debugging support for FitNesse. This is how it works:
- After running a test, replace the part
?test
in the URL with?debug
. The page will look like it’s hanging while the server is waiting for the debugger. - In your IDE, attach a remote debugger to the Fit-process.
- You can now debug your code just as normal
This turned out to be a lot harder with the newest version of FitNesse. To support Slim, this version introduces the concept of TestSystem
classes, of which there are two implementations, FitTestSystem
and SlimTestSystem
. These classes build up the (Java) command line to execute the test runner.
As of the current version of FitNesse, there is no plugin point to put new implementations of TestSystem
, so I had to change TestResponder
implementation of performExecution
quite a bit. The original has some caching and other freaky stuff which I finally gave up trying to override. Sadly, my implementation only works for Slim. For some reason, overriding FitTestSystem#buildCommand
causes FitNesse to hang. So: Use at your own risk.
To install the code, you have to modify the plugins.properties
file described above:
Responders = test:com.brodwall.fitnesse.InheritClasspathTestResponder, \
debug:com.brodwall.fitnesse.DebugTestResponder
Here is the awful implementation of DebugTestResponder
. I’ll see if I can get changes implemented in FitNesse to make it easier.
public class DebugTestResponder extends InheritClasspathTestResponder {
private static final String PATH\_SEPARATOR = System.getProperty("path.separator");
private static final int DEBUG\_PORT = 1044;
@Override
protected void performExecution() throws Exception {
TestSystem testSystem = createTestSystem();
// The following line is the correct behavior,
// but TestResponder.fastTest is private :-(
//testSystem.setFastTest(fastTest);
testSystem.setFastTest(false);
testSystem.getExecutionLog(classPath, TestSystem.getTestRunner(data));
testSystem.start();
if (testSystem.isSuccessfullyStarted()) {
addToResponse(HtmlUtil.getHtmlOfInheritedPage("PageHeader", page));
SetupTeardownIncluder.includeInto(data, true);
if (data.getContent().length() == 0)
response.add(formatter.messageForBlankHtml());
testSystem.sendPageData(data);
testSystemGroup.bye();
}
}
private TestSystem createTestSystem() throws Exception {
String testSystemName = TestSystem.getTestSystemName(data);
if ("slim".equalsIgnoreCase(TestSystem.getTestSystemType(testSystemName))) {
return new SlimTestSystem(page, this) {
@Override
protected String buildCommand(String program, String classPath) throws Exception {
return DebugTestResponder.this.buildCommand(program, classPath);
}
};
} else {
// For some reason, overriding FitTestSystem.buildCommand causes FitNesse to hang
return new FitTestSystem(context, page, this);
}
}
protected String buildCommand(String program, String classPath) throws Exception {
String debugOptions = "-Xrunjdwp:transport=dt\_socket,server=y,suspend=y,address="
+ DEBUG\_PORT;
return "java " + debugOptions + " " + "-cp " + classPath + PATH\_SEPARATOR
+ getInheritedClassPath() + " " + program;
}
}
In conclusion
It is possibly, you could even say easy, to make Maven and FitNesse play nice. The trick with InheritClasspathTestResponder
shows how you can stop maintaining the FitNesse classpath separate from the Maven classpath, which makes FitNesse into just another main-class in your system.
Sadly, the structure around the new TestSystem
classes needs a little improvement to support debugging.
Comments:
[Daniel Carleton] - May 27, 2011
Thanks for writing this post. Â Saved me a lot of pain! Â My use case is running FitNesse directly inside the IDE, which of course I’d like to have manage the classpath.
[Makarand Adnaik] - Aug 23, 2013
I think this works with old version, do you have anything which we can try with latest version, instead of adding dependency as a whole in the test we are looking for something we can refer as common repository. Please help. –Makarand
Johannes Brodwall - Aug 23, 2013
Thanks for the question, Makarand. This was actually answered in a comment on another post on the subject: “If you use !path {java.class.path} You get the classpath that fitnesse was called with, without the need for changing the fitnesse code.” Hope this helps.