Johannes Link

Software Therapist. Programmer. Supporter.


This is an old project which has not been updated or reconsidered in a long while. Please bear with me!

ReFit for FitNesse

Refactoring of FitNesse test pages can be very burdensome. This Groovy-based tool tries to mitigate some of the effort for doing reorganizations in test pages and suites.

How to Use It

Installation & Starting the ReFit console

  1. Make sure you have a Java Runtime Environment (>= 1.5) properly installed
  2. Download refit-0.9.zip
  3. Unzipping the file wherever you want results in a folder called refit-x.y
  4. Start a command shell and cd to refit-x.y
  5. Use ./refitConsole (or refitConsole.bat on Windows systems) to start up the ReFit console window

Loading a FitNesse Suite

ReFit is basically a scripting environment for running ReFit scripts. The console window works exactly like the Groovy console with the exception of an additional ReFit menu. Try "Refit->Load Suite..." to choose (a) your current FitNesse root directory and (b) the base suite you want to restrict your refactoring to (e.g. FitNesse). Separate subwikis by using a dot inbetween (e.g. FitNesse.AcceptanceTests) or leave the base suite name empty - in that case all tests within your FitNesse root directory will be considered.

Now ReFit will try to load all pages and sub pages of the specified suite and list all loaded pages in the console's stdout (the lower half of the app window). Now you are ready to type in - or load - any Groovy scripts into the upper half of the application window. ReFit will give you access to the loaded suite via the globally visible varaible suite.

Collecting information

Let's try our first script. Given a standard FitNesse installation (fitnesse20070619.zip), using "fitnesse/FitNesseRoot" as root dir and "FitNesse" as base suite, we try the following:

suite.pages.size()

will show "501" as result.

suite.pages.each {
  println it.fullName
}
null

will print out all 501 page names to stdout. Only the last few thousand characters will remain visible, though. I added the null line to make the scripts result value null instead of the whole collection of all pages.

Performing Refactorings

Our next task will be a refactoring: Change all fixtures named "Summary" - in other words: all tables whose first cell equals "Summary" - to "Digest":

suite.allFixtures.findAll { it.name.normalize() == "summary" }.each { fixture ->
  fixture.rename("Digest")
}

Running this script will open the refactorings view: a table enumerating all details of refactorings which have been performed but not yet committed; in this case 5 renamings. You can now choose to rollback or commit all listed refactorings by pressing the appropriate button. Committing will, as you probably expect, change all files in the file system and print out the update, remove and move messages to stdout.

A more complex example:

suite.pages.findAll { page ->
  page.name.normalize() == "setup" && page.fixtures.any { it.name.normalize() == "import"}
}.each {page ->
  page.fixtures[0].remove()
}

This script will find all "SetUp" pages that also have an "Import" fixture and then remove the first fixture of those pages. Your imagination is the limit!

Capabilities

What kind of refactorings are possible? You can be derive the details from the object model described below. Among ReFit's capabilities you find:

  • Renaming, moving & removing pages (test pages and suites)
  • Renaming & removing fixtures (i.e. tables)
  • Changing parameters of fixtures (i.e. the cells following a fixture's name)
  • Renaming, deleting, moving and inserting columns in column fixtures
  • Adding rows to a fixture
  • Removing rows from a fixture
  • Pruning empty lines from pages
  • Adding and removing non-fixture lines in pages

There is much more that is not supported explicitly (yet) but can be performed by doing a bit of Groovy scripting, e.g.: Melting fixtures, renaming variables or symbols, commenting in/out lines etc.

Basic Rules

ReFit tries to keep all FitNesse syntax working, like comments (#) and literals (!- -!). Thus, a table starting with "!" will keep the "!" during refactoring.

Most of the time you can just change a property to make the refactoring work, e.g. fixture.rename("new name") will have the same effect as fixture.name = "new name". This works only within the ReFit console; when you use ReFit as an external library you should use the specialized refactoring methods like rename(..) or fixture.replaceArgs(..).

Global Properties and Functions

There is one global property called suite that will be set to an instance of FitnesseSuite when a suite has been loaded.

There are a few global functions available in the console:

  • loadSuite(fitnesseRoot, suiteName) will load a suite and return the suite object. The suite property is being set implicitly. fitnesseRoot can be a File object or a String describing the absolute path name; suiteName is optional.
  • reloadSuite() will reload the current suite and get rid of all uncommitted refactorings.

Special String Methods

You have seen the normalize() method in use. There are a few of those special String methods available within the ReFit console:

  • normalize() will remove all spaces and convert everything to lower case
  • cleanUp() replaces all whitespace by a single space
  • isWikiName() returns true if a string is a valid wiki name

The ReFit Object Model

There are a couple of relevant classes. Below I enumerate their public properties and methods. The specified types might or might not be statically checked - Groovy allows both ways. However, most of them are not.

refit.FitnesseSuite

Public Properties:
  • List<Fixture> allFixtures: a collection of all fixtures in all pages
  • File fitnessRoot: the FitNesse root directory
  • List<FitnessePage> pages: a collection of all pages in the base suite
  • File suiteDir: the directory of the given base suite
  • FitnessePage suitePage: the page of the given base suite
Public Methods:
  • void commit(): commit all uncommitted refactorings in this suite
  • void eachFixture(Closure): iterate through all fixtures of all pages
  • void eachPage(Closure): iterate through all pages
  • void rollback(): commit all uncommitted refactorings in this suite

refit.FitnessePage

FitnessePage has a few subclasses which support the same interface.
Public Properties:
  • String name: the page's name
  • PageContent content: the page's actual content
  • File contentFile: the page's content file (if there is one)
  • List<Fixture> fixtures: a collection of the page's fixtures in the right order
  • File folder: the page's folder
  • String fullName: the page's full name including all super pages up to the given root
Public Methods:
  • void eachFixture(Closure): iterate through all fixtures of this page only
  • void moveTo(String): move page and all sub pages to new parent
  • void remove(): remove current page and all sub pages
  • void removeFixture(Fixture): remove a fixture from this page
  • void rename(String): give this page a new name

refit.PageContent

Public Properties:
  • List<Fixture> fixtures: a collection of the page's fixtures in the right order
  • List flow: a collection of Fixture and UnparsedLine instances which together form the complete contents of the page
  • FitnessePage page: the page to which this content belongs
Public Methods:
  • void removeFromFlow(Object): remove a Fixture or UnparsedLine from the flow. This is a valid refactoring.
  • void removeEmptyLinesBut(int numberOfLines): remove empty lines but leave up to numberOfLines in a row in the flow. This is a valid refactoring used to clean up pages.

refit.Fixture

Public Properties:
  • List<String> args: a collection of the fixture's arguments
  • String canonicalName: the fixture's normalized name (no spaces, all lower case)
  • List columns: a collection of the fixture's columns - if it is a column fixture - which are themselves lists of String objects
  • PageContent content: the page content to which the fixture belongs
  • boolean ignoreMarkup: is markup being ignored in this fixture?
  • String name: the fixture's name
  • FitnessePage page: the page object to which the fixture belongs
  • List rows: a collection of the fixture's rows which are themselves lists of String objects
Public Methods:
  • void addRow(List<String>): append new row to fixture. In the ReFit console you can as well just update the rows property.
  • int countColumns(): number of columns in this fixture
  • void insertColumn(Map params): insert a new column. Params allowed are 'name', 'position' and 'defaultValue'; all three are optional.
  • void remove(): remove this fixture from its page
  • void removeColumn(String): remove a column by name
  • void rename(String): give this fixture a new name
  • void renameColumn(String oldName, String newName): rename a column
  • void replaceArgs(List<String> newArgs): replace existing args with new ones. In the ReFit console you can as well just update the args property.
  • void replaceRows(List newRows): replace existing rows with new ones. In the ReFit console you can as well just update the rows property.

refit.UnparsedLine

Public Properties:
  • String text: the actual String contents of a line. Changing this property will not automatically be considered a refactoring.

Open Issues

  • No connection to any VCS so far. Checking in and out must be done manually.
  • Properties of test pages are not being considered yet.

Release Notes

Version 0.9 2008-03-04

  • First public version. I'm trying to collect feedback and see if ReFit is interesting to others at all.