Extending SQLUnit: Writing your own Matcher

Note

If you have written a Matcher that does something unique, and would like to contribute it to the SQLUnit project, it would be gratefully accepted and credit given.

Writing your own Matcher is easy. The Matcher simply specifies the match operation by implementing the isEquals(String,String,Map) method in the net.sourceforge.sqlunit.IMatcher interface. Specifying where the matcher should be invoked can be done from the SQLUnit specification file itself.

We will illustrate the process by taking the PercentageRangeMatcher class supplied with the SQLUnit distribution and annotating it with comments that may be helpful in writing your own matcher.


/*
 * $Id: sqlunit-book.xml,v 1.100 2006/04/30 22:25:54 spal Exp $ (1) 
 * $Source: /cvsroot/sqlunit/sqlunit/docs/sqlunit-book.xml,v $
 * SQLUnit - a test harness for unit testing database stored procedures.(2)
 * Copyright (C) 2003  The SQLUnit Team
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package net.sourceforge.sqlunit.matchers; (3)

import java.util.Map;  (4)
import net.sourceforge.sqlunit.IErrorCodes;
import net.sourceforge.sqlunit.IMatcher;
import net.sourceforge.sqlunit.SQLUnitException;

/** (5)
 * The PercentageRangeMatcher is an implementation of the IMatcher interface
 * used to define rulesets for matching columns in SQLUnit. This matcher
 * will accept a percentage tolerance value and check to see that the target 
 * is within (+/-) tolerance percent of the source.
 * Arguments:
 * pc-tolerance : a percentage tolerance value.
 * @author Sujit Pal (spal@users.sourceforge.net)
 * @version $Revision: 1.100 $
 */
public class PercentageRangeMatcher implements IMatcher { (6)

    /**
     * Default constructor as per contract with IMatcher.
     */
    public PercentageRangeMatcher() {;}

    /**
     * Returns true if the value of the target is withing (+/-) a specified
     * tolerance value of the source. Note that in this case, the source,
     * target and tolerance must all be numeric values.
     * @param source the String representing the source to be matched.
     * @param target the String representing the target to be matched.
     * @param args a Map of name value pairs of arguments passed in.
     */
    public boolean isEqual(String source, String target, Map args)
            throws SQLUnitException { (7)

        String aTolerance = (String) args.get("pc-tolerance"); (8)
        if (aTolerance == null) {
            throw new SQLUnitException(IErrorCodes.MATCHER_EXCEPTION,
                new String[] {this.getClass().getName(),
                "Value for key 'pc-tolerance' is NULL"});
        }
        // is tolerance a float?
        float iTolerance = 0;
        try {
            iTolerance = Float.parseFloat(aTolerance);
        } catch (NumberFormatException e) {
            throw new SQLUnitException(IErrorCodes.MATCHER_EXCEPTION,
                new String[] {this.getClass().getName(),
                "Value of key 'pc-tolerance' is not a FLOAT"});
        }
        // cannot have the tolerance exceed 100
        if (iTolerance > 100.0) {
            throw new SQLUnitException(IErrorCodes.MATCHER_EXCEPTION,
                new String[] {this.getClass().getName(),
                "Value of key 'pc-tolerance' must be between 0 and 100"});
        }
        // is the source a float? (9)
        float iSource = 0;
        try {
            iSource = Float.parseFloat(source);
        } catch (NumberFormatException e) {
            throw new SQLUnitException(IErrorCodes.MATCHER_EXCEPTION,
                new String[] {this.getClass().getName(),
                "Value of 'source' is not a FLOAT"});
        }
        // is the target an integer?
        float iTarget = 0;
        try {
            iTarget = Float.parseFloat(target);
        } catch (NumberFormatException e) {
            throw new SQLUnitException(IErrorCodes.MATCHER_EXCEPTION,
                new String[] {this.getClass().getName(),
                "Value of 'target' is not a FLOAT"});
        }
        // return the match (10)
        return ((iTarget >= (iSource - (iTolerance * 100))) &&
            (iTarget <= (iSource + (iTolerance * 100))));
    }
}
      

(1)
In general, if you are contributing a matcher to the SQLUnit project, please include the Id and Source CVS tags. This will allow us to track version revisions in SQLUnit. Even if you dont plan to contribute the matcher to the SQLUnit project, its a good practice to include the tags for whatever Source Code Control System you are using.
(2)
Please include this boilerplate in your code if you are planning to contribute the matcher to the SQLUnit project. This indicates that this code is now covered by the GNU General Public License. If you are not planning to contribute the Matcher, please ignore this callout.
(3)
The package statement identifies the package in which your matcher class will live in. It can be anything you want, but your CLASSPATH must contain this package when running SQLUnit with your new user-defined Matcher class.
(4)
You will need to import the following classes from the net.sourceforge.sqlunit package. IMatcher is the interface which your matcher class is implementing. SQLUnitException is the Exception class thrown by SQLUnit applications, and IErrorCodes is another interface which contains the definition of the Error Codes thrown by SQLUnit. It is important to throw SQLUnitExceptions from your Matcher class because otherwise your exceptions will not be reported by SQLUnit.
(5)
Because of the weak coupling between any Matcher and the rest of SQLUnit, the class documentation is probably the best place you have to provide the user of your Matcher class with useful information relating to the use of the Matcher. The class documentation in this case defines what the Matcher does, and provides the information about what keys need to be passed in to the Matcher for it to do its work.
(6)
Your Matcher class needs to implement the net.sourceforge.sqlunit.IMatcher interface, otherwise it will not be visible to SQLUnit.
(7)
Your Matcher class needs to implement the only method in the IMatcher interface, which is the boolean isEqual(String,String,Map). The first argument represents the source column value to match, the second the target column value, and the third is a Map of key-value pairs which contain additional information needed by the isEqual method. If the actual comparison needs to be made on numeric objects, as is the case with the PercentageRangeMatcher, then your Matcher should convert to the appropriate class.
(8)
The weak coupling between the SQLUnit code and the Matcher code is by design. It was necessiated by the need to make the Matchers be very flexible and to keep the SQLUnit XML code simple to use. As a result, the Matcher must make sure that it can use the arguments being passed to it. If not, it should abort with a SQLUnitException (IErrorCodes.MATCHER_EXCEPTION) specifying the class name and the Matcher specific exception message.
(9)
As mentioned above, the Matcher code is responsible for converting from the supplied String value to whatever form it needs. In this case, it converts the source and target String values to Floats. If conversion fails, then it should throw a SQLUnitException (IErrorCodes.MATCHER_EXCEPTION) specifying the class name and the Matcher specific exception message.
(10)
This computes the value of the match and returns it.