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:
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.
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;Now all we need to do is add a line in the plugin.properties file:
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 + "}";
}
}
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:
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.
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 !- -!).
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
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.
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).
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.
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.
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.
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.
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.
Glad it worked out for you. With regard to the fixture names, have you tried Gracefule Names?
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|
Hi Hari,
Nice work!
Pete
Thanks Pete. Following your lead
HI
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?
Hmm, I haven't touched this stuff for a while, but maybe you should only render the string once in render().
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?
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...
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
Post a Comment
<< Home