Application Driven Testing of Drools with Fitnesse – part 2


Other Posts in this Series

Application Driven Testing of Drools with Fitnesse – part 1

Overview

In this post we will create a JUnit Test Case that fires a simple rule.

Step 1

First let’s create the classes and files we will need for our rule example:

  • Person.java – this will be a fact that is inserted into the Rule Engine
  • FileManager.java – contains utility methods
  • RuleRunner.java – build the KnowledgeBase, and create StatefulKnowledgeSessions
  • test1.drl – a simple rule

Create the following classes in their respective classes in src/main/java

Person.java

package com.skills421.model;

public class Person
{
private String firstname;
private String lastname;
private int age;
private double cmHeight;
private double kgWeight;

public Person()
{

}

public Person(String firstname, String lastname, int age, double cmHeight, double kgWeight)
{
super();
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
this.cmHeight = cmHeight;
this.kgWeight = kgWeight;
}

public String getFirstname()
{
return firstname;
}

public void setFirstname(String firstname)
{
this.firstname = firstname;
}

public String getLastname()
{
return lastname;
}

public void setLastname(String lastname)
{
this.lastname = lastname;
}

public int getAge()
{
return age;
}

public void setAge(int age)
{
this.age = age;
}

public double getCmHeight()
{
return cmHeight;
}

public void setCmHeight(double cmHeight)
{
this.cmHeight = cmHeight;
}

public double getKgWeight()
{
return kgWeight;
}

public void setKgWeight(double kgWeight)
{
this.kgWeight = kgWeight;
}

@Override
public String toString()
{
return String.format("Person [firstname=%s, lastname=%s, age=%s, cmHeight=%s, kgWeight=%s]", firstname, lastname, age, cmHeight, kgWeight);
}

}

FileManager.java

package com.skills421.managers;

import java.io.File;
import java.io.IOException;

public class FileManager
{
public static String replaceAllDotsWithPathSeparator(String packagename)
{
// if we are on a unix box, replace all .'s in the package name with /'s
// if we are on a windows box replace all .'s in the package name with
// \'s

if (File.separator.equals("/"))
{
packagename = packagename.replaceAll("\\.", "/");
}
else
{
packagename = packagename.replaceAll("\\.", "\\\\");
}

return packagename;
}

public static String convertRulePackageAndFileToFullPathname(String packagename, String filename)
{
// turn packagename to file path
StringBuilder fullpathnameSB = new StringBuilder();

fullpathnameSB.append(FileManager.replaceAllDotsWithPathSeparator(packagename))
.append(File.separator)
.append(filename);

return fullpathnameSB.toString();
}

public static String convertRulePackageAndFileToFullPathname(String packageAndFilename) throws IOException
{
// in the event that we get passed a filepath that actually exists
// just return it

if (new File(packageAndFilename).exists())
return packageAndFilename;

// remove the extension

if(packageAndFilename.indexOf(".")==-1)
{
throw new IOException("No file extension provided for "+packageAndFilename);
}

int extPstn = packageAndFilename.lastIndexOf(".");
String packagename = packageAndFilename.substring(0,extPstn);
String ext = packageAndFilename.substring(extPstn);

StringBuilder fullpathnameSB = new StringBuilder();

fullpathnameSB.append(FileManager.replaceAllDotsWithPathSeparator(packagename))
.append(ext);

return fullpathnameSB.toString();
}
}

RuleRunner.java

package com.skills421.rules;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.drools.KnowledgeBase;
import org.drools.KnowledgeBaseFactory;
import org.drools.builder.KnowledgeBuilder;
import org.drools.builder.KnowledgeBuilderError;
import org.drools.builder.KnowledgeBuilderFactory;
import org.drools.builder.ResourceType;
import org.drools.decisiontable.InputType;
import org.drools.decisiontable.SpreadsheetCompiler;
import org.drools.definition.KnowledgePackage;
import org.drools.io.Resource;
import org.drools.io.ResourceFactory;
import org.drools.runtime.StatefulKnowledgeSession;

import com.skills421.managers.FileManager;

public class RuleRunner
{
private static Logger logger = LogManager.getLogger(RuleRunner.class);

private KnowledgeBase kbase;
private KnowledgeBuilder kbuilder;

public RuleRunner()
{
kbase = KnowledgeBaseFactory.newKnowledgeBase();
kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
}

public List<String> buildKnowledgeBase(String[] decisionTables, String[] drlFiles)
{
List<String> buildErrors = new ArrayList<String>();

if (decisionTables != null)
{
logger.info("*** Adding Decision Tables ***");

for (int i = 0; i < decisionTables.length; i++)
{
String decisionTable = decisionTables[i];
Resource xlsResource = ResourceFactory.newClassPathResource(decisionTable, RuleRunner.class);

kbuilder.add(xlsResource, ResourceType.DTABLE);
}
}

if (drlFiles != null)
{
logger.info("*** Adding DRL Files ***");

for (int i = 0; i < drlFiles.length; i++)
{
try
{
String drlFile = FileManager.convertRulePackageAndFileToFullPathname(drlFiles[i]);
if (drlFile.startsWith(File.separator) || drlFile.toUpperCase().startsWith("C:"))
{
kbuilder.add(ResourceFactory.newFileResource(new File(drlFile)), ResourceType.DRL);
}
else
{
kbuilder.add(ResourceFactory.newClassPathResource(drlFile, RuleRunner.class), ResourceType.DRL);
}
}
catch (IOException e)
{
buildErrors.add(e.getMessage());
}

}
}

logger.info("*** Checking for Errors ***");
if (kbuilder.hasErrors())
{
logger.info("*** !!! Build Errors !!! ***");
for (KnowledgeBuilderError error : kbuilder.getErrors())
{
logger.info(error);
buildErrors.add(error.toString());
}
}

logger.info("*** Loading KnowledgePackages ***");
Collection<KnowledgePackage> pkgs = kbuilder.getKnowledgePackages();
kbase.addKnowledgePackages(pkgs);

return buildErrors;
}

public List<String> buildKnowledgeBase(List<String> decisionTables, List<String> rules)
{
String[] decisionTablesArr = null;
String[] rulesArr = null;

if (decisionTables != null)
decisionTablesArr = decisionTables.toArray(new String[decisionTables.size()]);
if (rules != null)
rulesArr = rules.toArray(new String[rules.size()]);

return this.buildKnowledgeBase(decisionTablesArr, rulesArr);
}

public static String getDRLFromDecisionTable(String decisionTable)
{
String drlContent = null;

try
{
SpreadsheetCompiler sc = new SpreadsheetCompiler();

Resource xlsResource = ResourceFactory.newClassPathResource(decisionTable, RuleRunner.class);
drlContent = sc.compile(xlsResource.getInputStream(), InputType.XLS);
}
catch (Exception e)
{
e.printStackTrace();
}

return drlContent;
}

public StatefulKnowledgeSession newKnowledgeSession()
{
return kbase.newStatefulKnowledgeSession();
}

public static void logDrlContent(String decisionTable) throws IOException
{
String drlContent = RuleRunner.getDRLFromDecisionTable(decisionTable);
logger.info("DRLContent: " + drlContent);
}
}

test1.drl

in src/test/resources create test1.drl in the sub-folder com/skills421/rules

package com.skills421.rules

import com.skills421.model.Person
import java.util.List

dialect "mvel"

global List OUTPUTLIST

/*
* This rule checks that a person is >= 21
*/
rule "Person is 21"
when
$person : Person(age>=21)
then
OUTPUTLIST.add(String.format("%s %s",$person.firstname,$person.lastname));
end

Testing FileManager

FileManager provides 3 static methods that are used to determine the file location of a drl file from the package name and filename.

our test1.drl is in the package com.skills421.model, so the actual file should be com/skills421/rules/test1.drl somewhere on the classpath.

The three static methods in FileManager are as follows:

  • replaceAllDotsWithPathSeparator
    this will take a String of the format com.skills421.rules and convert it to com/skills421/rules for a Unix platform and \com\skills421\rules for a Windows platform.
  • convertRulePackageAndFileToFullPathname(packagename,filename)
    this will take a package name – e.g. com.skills421.rules and a filename – e.g. test1.drl and produce a full path of com/skills421/rules/test1.drl
  • convertRulePackageAndFileToFullPathname(packageAndFile)
    this will take a package name – e.g. com.skills421.rules.test1.drl and produce a full path of com/skills421/rules/test1.drl

Create the file FileManagerTest.java in the appropriate package in src/test/java

FileManagerTest.java

package com.skills421.managers;

import static org.junit.Assert.*;

import java.io.IOException;

import org.junit.Test;

public class FileManagerTest
{

@Test
public void testReplaceAllDotsWithPathSeparator()
{
String packagename = "com.skills421.rules";

String path = FileManager.replaceAllDotsWithPathSeparator(packagename);

assertEquals("com/skills421/rules", path);
}

@Test
public void testConvertPackageNameToDrlFilePathOneArg()
{
String packagename = "com.skills421.rules.test1.drl";

try
{
String drlFilePath = FileManager.convertRulePackageAndFileToFullPathname(packagename);

assertEquals("com/skills421/rules/test1.drl", drlFilePath);
}
catch (IOException e)
{
fail(e.getMessage());
}
}

@Test
public void testConvertPackageNameToDrlFilePathTwoArgs()
{
String packagename = "com.skills421.rules";
String filename = "test1.drl";

String drlFilePath = FileManager.convertRulePackageAndFileToFullPathname(packagename, filename);

assertEquals("com/skills421/rules/test1.drl", drlFilePath);
}

}

Now right click on the file and run it as a JUnit test.  You should see a green bar indiciating that all the tests ran successfully.

Testing RuleRunner

RuleRunner provides a number of methods of which we are currently interested in the following:

  • buildKnowledgeBase
  • newKnowledgeSession

We need to write a couple of JUnit tests to firstly build the KnowledgeBase from test1.drl, and secondly run the rules defined in test1.drl.

Create the file RuleRunnerTest.java in the appropriate package in src/test/java

RuleRunnerTest.java

package com.skills421.rules;

import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.List;

import org.drools.runtime.StatefulKnowledgeSession;
import org.junit.Before;
import org.junit.Test;

import com.skills421.model.Person;

public class RuleRunnerTest
{
private RuleRunner ruleRunner;

@Before
public void setupRunner()
{
ruleRunner = new RuleRunner();
}

@Test
public void testBuildKnowledgeBase()
{
String[] decisionTables = null;
String[] drlFiles = new String[] { "com.skills421.rules.test1.drl" };

List<String> errors = ruleRunner.buildKnowledgeBase(decisionTables, drlFiles);

assertTrue(errors.size()==0);
}

@Test
public void testRunRules()
{
String[] decisionTables = null;
String[] drlFiles = new String[] { "com.skills421.rules.test1.drl" };

List<String> errors = ruleRunner.buildKnowledgeBase(decisionTables, drlFiles);

assertTrue(errors.size()==0);

StatefulKnowledgeSession ksession = ruleRunner.newKnowledgeSession();

Person p1 = new Person("Jon","Doe",18,1.8,80);
Person p2 = new Person("Jane","Doe",22,1.9,70);

List<String> outputList = new ArrayList<String>();

ksession.setGlobal("OUTPUTLIST", outputList);

ksession.insert(p1);
ksession.insert(p2);

ksession.fireAllRules();

assertEquals(1, outputList.size());

assertEquals("Jane Doe", outputList.get(0));
}
}

The first test simply passes test1.drl to an instance of RuleRunner and builds the KnowledgeBase, checking that there are no errors.

The second test is a little more involved and obtains a StatefulKnowledgeSession from the KnowledgeBase.  It first inserts a Global List into the Session which is used for retrieving a list of String output from the Rule Engine.  Next, it inserts 2 Person objects (facts), and finally it fires the rules.

With our rule as specified in test1.drl we are expecting one Person to satisfy our rules, namely “Jane Doe”.  The rule, on finding a match will insert the full name of the matched person into the outputList.

So, once our rules have fired, we can check our outputList for 1 match – “Jane Doe”.

Right click on RuleRunnerTest and run as Junit Test.

You should see a green bar indicating that the JUnit tests ran successfully.

Download the Source

The source for part-2 can be downloaded form GitHub here.

Advertisement

2 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s