Oliver Wehrens
Seasoned Technology Leader. Mentor. Dad.
Oliver Wehrens

Test JBoss Rules 5 (or Drools) with TestNG

- 4 min
We have been using our own flavor of Fit for Rules (which is build on top of fit) for about 1 1/2 years now to test our business logic written in JBoss Rules 5. It's relatively easy to get the Business Analyst on board since he is using his tool (which is Microsoft Excel) to communicate test cases for the rules. So in theory, he writes the tests in Excel, we do the rules coding and voila, all tests turn green.

Reality is, we have to tweak the Excel sheets. We need to put in imports of our fact model, insert facts and create objects within that not so programmer friendly table environment. A couple of days ago we got the request to tweak some rules and we all had to start doing rules again (and we used to use Eclipse for writing rules because that’s the only IDE having a plugin for that).

After half a day of coding Java syntax in Excel sheets we decided that the ramp up time for the not so knowledgeable rules/fit programmers like me is too much. With debugging, copy and paste we spent easily 5-10 times more time on making the tests work than writing the code itself. Test driven design is not really an option here, since you need to know the imports of the rules file to get the sheet even to compile.

So what did we do ? Well why not try to get things working the way we used to do it ? TestNG anyone ?

There are many pros to use unit testing but also some cons. The biggest issue is that we will loose the direct communication to the business analyst. It’s always better if someone else writes the test and I just have to implement the solution. Maybe we can find another solution involving Active Spec or DSL. For now we stick to unit tests and the task the we have to make sure we convert every Excel test case to java code (but hey, that’s what our code reviews are for).

Checkout our current base class for testing our rules:

public abstract class AbstractRulesTest {
   public abstract String[] getRulesFileNames();
   private final String GET_FINDINGS = "import com.maxheapsize.RulesFinding;" +
                                                                           "query \"getAllRulesFindings\"\n" +
                                                                           "   finding : FRulesFinding()\n" +
                                                                           "end";

   private static Logger LOG = Logger.getLogger(AbstractRulesTest.class);

   public final List<FRulesFinding> fireRules(Set factsForWorkingMemory) {
       KnowledgeBase ruleBase = setUpKnowledgeBase();
       return fireRules(ruleBase, factsForWorkingMemory);
   }

   public KnowledgeBase setUpKnowledgeBase() {
       KnowledgeBaseConfiguration configuration = KnowledgeBaseFactory.newKnowledgeBaseConfiguration();
       KnowledgeBase ruleBase = KnowledgeBaseFactory.newKnowledgeBase(configuration);

       KnowledgeBuilder build = KnowledgeBuilderFactory.newKnowledgeBuilder();
       build.add(ResourceFactory.newReaderResource(new StringReader(GET_FINDINGS)), ResourceType.DRL);
       String[] fileNames = getRulesFileNames();
       for (String fileName : fileNames) {
           File userDefinedFile = new File(fileName);
           build.add(ResourceFactory.newFileResource(userDefinedFile), ResourceType.DRL);
       }

       handleBuilderErrors(build);

       ruleBase.addKnowledgePackages(build.getKnowledgePackages());
       return ruleBase;
   }

   private void handleBuilderErrors(KnowledgeBuilder build) {
       if (build.hasErrors()) {
           KnowledgeBuilderErrors knowledgeBuilderErrors = build.getErrors();
           for (KnowledgeBuilderError knowledgeBuilderError : knowledgeBuilderErrors) {
               int[] ints = knowledgeBuilderError.getErrorLines();
               LOG.error("Error at : "+ints[0]+" : "+ints[1]);
               LOG.error(knowledgeBuilderError.getMessage());
           }
       }
   }

   private List<FRulesFinding> fireRules(KnowledgeBase ruleBase, Set facts) {
       List<FRulesFinding> result = new ArrayList<FRulesFinding>();
       StatefulKnowledgeSession statefulSession = ruleBase.newStatefulKnowledgeSession();
       for (Object fact : facts) {
           statefulSession.insert(fact);
       }
       statefulSession.fireAllRules();

       QueryResults results = statefulSession.getQueryResults("getAllRulesFindings");
       try {
           FRulesFinding finding = (FRulesFinding) results.iterator().next().get("finding");
           result.add(finding);
       }
       catch (NoSuchElementException e) {
           result = new ArrayList<FRulesFinding>();
       }
      return result;
   }
}

All my rules insert a RulesFinding (and only one at the moment) into the working memory when triggered. The rest is pretty easy. You subclass it, overwrite getRulesFileNames and call fireRules with a set of objects (your tests) which need be insert into the working memory. To get the finding back you need to execute an already inserted query which needs to have an identifier (line 3, 20, 52, 54). It will contain the result of your rule execution.

Sample code would look like this:

public class RulesTest extends AbstractRulesTest {

   private Set facts;

   @BeforeMethod
   public void setUp() {
       facts = new HashSet();
   }

   @Override
   public String[] getRulesFileNames() {
       return new String[]{
               "src/main/rules/myrules.drl",
               "src/main/rules/generealRules.drl"
           };
  }

   @Test
   public void testDemoRule() {

       FMyFact myFact = new FMyFact();
    myFact.setColor("green");
       facts.add(myFact); // add all your facts here

       List<FRulesFinding> findings = fireRules(facts);
       Assert.assertTrue(findings.size() == 1);
       FRulesFinding finding = findings.get(0);
       Assert.assertTrue(finding.getStatus() == FStatus.OK);
   }
 }

Depending on how you cut your rules you can extract the assertion of the status.

Each test case in Excel now takes about 5-10 lines of java code. Considering we are covering each rule with about 5-15 test cases and boundary conditions this amounts to 75-150 lines of test code. I take that any day over programming in Excel.