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.

20 Comments:

Anonymous Anonymous said...

Hi Rong,
Your DateExpressionWidget is very useful but if i try to use this expression !date{today} within a table then it's being treated as a regular string and not converted to a date. I tried declaring it to a variable but still no use. Pls let me know how to use this expression in fit tables.

1:26 PM  
Blogger Rong said...

Does it work when you put it outside the table? The only thing I can think of is maybe you are escaping the string inside the table (the verbatim tag in FitNess !- -!).

1:38 PM  
Anonymous Anonymous said...

It works pretty fine when I use it outside the table. But when I try to set it to a variable or use it inside a table it's not getting converted to date. I have posted my test cases below

!define myDate {!date(today)}

!|DbColumnFixture|
|rowNum|columnName|getValue()|
|1|START_DATE|!date(today)|

I am using parenthesis instead of curly braces since it conflicts with curly braces required by !define

1:46 PM  
Blogger Rong said...

Oh I see what the problem is. The code I posted only supports curly braces. So if you switch around the curly braces and parenthesis it should work:

!define myDate (!date{today})

!|DbColumnFixture|
|rowNum|columnName|getValue()|
|1|START_DATE|!date{today}|

It should be pretty straightforward to fix that in the regular expressions.

1:56 PM  
Blogger Rong said...

If you replace the first two lines in the class to this:

private static final String DATE = "\\s*today\\s*(?:[+-]\\s*[1-9][0-9]*\\s*)?";
public static final String REGEXP = "!date(?:(?:\\{" + DATE + "\\})|(?:\\(" + DATE + "\\)))";
private static final Pattern pattern = Pattern.compile("!date[\\{\\(](.*)[\\}\\)]", Pattern.DOTALL + Pattern.MULTILINE);

That should allow you to use parenthesis too. But I haven't tested this since I don't have the environment set up any more (my hard disk crashed a few days ago).

2:09 PM  
Anonymous Anonymous said...

I changed the regular expression to accept paranthesis first itself but the results remain the same. I guess if we use it in tables fitnesse considers it as plain string and doesn't apply widgets conversion to it. Pls test this in a table or assign it to a variable when you get a chance and let me know. Thanks a lot for your prompt replies and support.

2:27 PM  
Blogger Rong said...

Here is one of our test cases:

!define date (!date{today+20})

|send|1${date}DFWAUS|

|show as rows|response rows|

It also works fine if I put the expression inside the table instead of using a variable.

Are you able to use variables inside your tables at all? The only other thing I can think of is maybe your FitNess version is too old.

2:40 PM  
Anonymous Anonymous said...

Hi Rong,
The same works for me too. The question is how to use it in a table which has other set of values. (e.g.)

!|DbFixture|
|rowNum|columnName|getValue()|
|1|START_DATE|!date{today}|

In your example the line
|send|1${date}DFWAUS| is standing alone and hence it works. If you try to attach this row to any other table then the conversion is not working.

Questions
1) What kind of fixture you are using?
2) Is "send" a method?
3) If i try to split my row where I use the date then the expression gets converted to proper date but it's not accepted as part of the table, it's reporting it as an error.

Once again tons of thanks for you support.

4:44 PM  
Blogger Rong said...

Ok, I finally tried you example in our FitNesse server. I think the problem is with the exclamation mark in front of the fixture. If I remove it then it works fine:

|DbFixture|
|rowNum|columnName|getValue()|
|1|START_DATE|!date{today}|

What is the exclamation mark for? I've never seen a use like that.

7:27 AM  
Anonymous Anonymous said...

U DA MAN. !--! works instead of !. If your fixture class name has two words like ColumnFixture then we can prefix ! (only once before the begining of the table) or use !--! (Need to use this to wrap each and every two letter word so that wiki doesn't convert it into a link) to be treated as plain text else wiki will put a ? at the end and treat it as a link. If I change ! to !--! then the conversion works pretty fine.

In one scenario I am using DoFixture and SetUpFixture. In SetUpFixture I am having like 50 rows of values and it's tough to wrap all the values within !--!. (e.g.)

!|set target|
|param|param value|
|LoanInvestorLoanId|RL_GENERATE|
|LoanLockNumber|$Loan1.LoanInvestorLoanId$|
|LoanPurchasedBalance|500046.0|
|LoanPropAddress|Test|
|LoanArmLookbackPerCd|9|
|LoanActualBalPaidThruDate|!date{today+12d}|

(In this case conversion doesn't work since I use ! at the begining of the table)

TO

|!-set target-!|
|param|param value|
|!-LoanInvestorLoanId-!|RL_GENERATE|
|!-LoanLockNumber-!|!-$Loan1.LoanInvestorLoanId$-!|
|!-LoanPurchasedBalance-!|500046.0|
|!-LoanPropAddress-!|Test|
|!-LoanArmLookbackPerCd-!|9|
|!-LoanActualBalPaidThruDate-!|!date{today+12d}|

(In this case conversion works)

Any how finally got it working. Once again thanks a lot.

10:10 AM  
Blogger Rong said...

Glad it worked out for you. With regard to the fixture names, have you tried Gracefule Names?

7:46 AM  
Anonymous Anonymous said...

I did try Graceful Names now and it's working fine for ColumnFixture but not for DoFixture and SetUpFixture. (e.g.)

|Import|
|a.b.c.d.e.f.g.dbfixtures|


|!-Db column fixture-!|
|rowNum|columnName|getValue()|
|1|START_DATE|!date{today+12d}|
|1|NEXT_PMT_DUE_DATE|2006-04-15|
|1|AMORT_TERM_MO|360|
|1|REM_TERM_MO|360|
|1|CURRENT_BALANCE|500046.0|
|1|CURRENT_INTEREST_RATE|6.375|
|1|ACTUAL_BALANCE|208600.0|
|1|ACTUAL_BAL_PAID_THRU_DATE|2006-03-15|
|1|SCHEDULED_BALANCE|500046.0|
|1|VALUE|3307895|

8:18 AM  
Anonymous Anonymous said...

Hi Hari,
Nice work!
Pete

6:22 PM  
Anonymous Anonymous said...

Thanks Pete. Following your lead

4:37 PM  
Anonymous Anonymous said...

HI

7:48 PM  
Anonymous Anonymous said...

I really like this. I tried creating my own widget that returns a randomly created string. The problem is that when I assign this to a variable it doesn't evaluate the widget first. So what happens is that the variable gets assigned to the string !randomString(18) which gets evaulated where the variable is used. What I would like is that a random string gets assigned to the variable so that same random string would be used over and over by the variable. Any ideas?

8:11 AM  
Blogger Rong said...

Hmm, I haven't touched this stuff for a while, but maybe you should only render the string once in render().

2:23 PM  
Anonymous Anonymous said...

I too have a need for the literal value of a widget being assigned to a variable rather than the widget being reevaluated every time. Anyone managed this?

5:29 PM  
Anonymous Anonymous said...

None of the workarounds suit my setup.

All my fixtures have !| FixtureName| and hence I can't include the date widget in a table. Nor will it assign to a variable.

So I can plaster any date anywhere except where it can be evaluated for test purposes.

Now if only the whole widget thing in fitnesse was properly documented...

3:40 AM  
Anonymous Anonymous said...

Hello,

I faced the same issue, because as it is stated on FitNesse.MarkupVariables:
“The text in a variable is never interpreted as wiki markup. It is always raw literal text.”
So I guess that !define has to be overridden in order to be able to evaluate wiki expressions in the variable definition body.

Andras

12:39 AM  

Post a Comment

<< Home