Wednesday, March 08, 2006

Extending FitNesse through widgets

If you've been doing XP/Agile development for a while, chances are you've run into the "holy trinity" of acceptance testing: Fit/FitNesse/FitLibrary. In a nutshell, Fit allows customers and testers to write tests as HTML tables, which are hooked to your code through "fixtures"; FitNesse builds on top of Fit, providing a wiki server so you can write tests as wiki pages and run them directly from the browser; and finally FitLibrary is a set of cool fixtures and runners that extend both Fit and FitNesse. Used together, these tools provide a compelling environment for developing customer-specified acceptance tests.

For our projects, we've been mostly using FitNesse as the "engine" to write, run and organize our tests. It has served us well. But last week we ran into a problem that seemed impossible to solve at the time. It turns out that FitNesse already has a nice extension mechanism to solve the problem elegantly.

The problem we were facing was this. From the FitNesse tests, we are sending commands to the backend system and examing the responses that come back. Some of the commands have a date or date range in them; and for some tests, the backend actually gets the data from another test system. The problem is, the other test system only holds a few days worth of data centered around the current date. If we hard-code the dates in the commands, the tests might work for now, but eventually they'll fail once the dates fall out of the rolling window in the other test system. When that happends, we can either update the FitNesse tests with new dates, which would be a pain in the neck; or figure out something that would work automatically.

After stumbling around with markup variables and trying to understand how to define variables in SystemProperties, which didn't work for our purposes, we finally hit upon the idea of defining expressions inside the FitNesse tests. For example, if we could do

{today}
{today + 4}
{today -3}
...

and replace those with actual date strings at runtime, that'd be perfect.

So again we stumbled upon FitNesse's plugin structure and found that a custom WikiWidget would work perfectly. You see, every wiki syntax that FitNesse understands is internally represented by a WikiWidget. To make FitNesse understand our custom date expression, all we need to do is write our own custom WikiWidget that understands those date expressions. A little more digging led us to write this class:
package fitnesse.wikitext.widgets;

import java.text.*;
import java.util.Calendar;
import java.util.regex.*;

import fitnesse.html.HtmlUtil;
import fitnesse.wikitext.widgets.ParentWidget;

public class DateExpressionWidget extends ParentWidget {
public static final String REGEXP = "!date\\{\\s*today\\s*(?:[+-]\\s*[1-9][0-9]*\\s*)?\\}";
private static final Pattern pattern = Pattern.compile("!date\\{(.*)\\}", Pattern.DOTALL + Pattern.MULTILINE);
private static final DateFormat format = new SimpleDateFormat("ddMMM");

private String expression;
private String renderedText;
private boolean rendered;

public DateExpressionWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = DateExpressionWidget.pattern.matcher(text);
if (match.find())
expression = match.group(1);
}

public String render() throws Exception {
if (!rendered)
doRender();
return renderedText;
}

private void doRender() throws Exception {
String value = parseDate(expression);
if (value != null) {
addChildWidgets(value);
renderedText = childHtml();
}
else
renderedText = makeInvalidExpression(expression);
rendered = true;
}

private String parseDate(String expression) {
int offset = 0;
if (expression.indexOf("+") != -1)
offset = Integer.parseInt(expression.substring(expression.indexOf("+") + 1).trim());
else if (expression.indexOf("-") != -1)
offset = - Integer.parseInt(expression.substring(expression.indexOf("-") + 1).trim());

Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, offset);
return format.format(cal.getTime()).toUpperCase();
}

private String makeInvalidExpression(String name) throws Exception {
return HtmlUtil.metaText("invalid date expression: " + name);
}

public String asWikiText() throws Exception {
return "!date{" + expression + "}";
}
}
Now all we need to do is add a line in the plugin.properties file:

WikiWidgets=fitnesse.wikitext.widgets.DateExpressionWidget

Then we can write our date expressions anywhere on the wiki page (works for variables too):

!date{today}
!date{today + 4}
!date{today - 3}

I don't know about you, but that seems pretty cool to me. Of course, you can even extend this further to make it a full-blown expression language. Well, if you really have the need.