Test JBoss Rules 5 (or Drools) with TestNG

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.

How pair programming can help you to get into Test Driven Development

At my current job management buys into Test driven development. It slowly starts to become our primary development style.

Some advantages of TDD for me are:

  • Focus on problem solution
  • No unnecessary development for use cases dreamed up by the developer
  • Always make sure your code fulfills the requirements (tests)
  • If you have tests you can safely refactor your code at any time

Even though we all knew about the advantages of TDD, we figured out that we do not always write tests first. While this might me acceptable in some situations like UI tests, most of the time it is not desired.
After seeing that problem for a couple of weeks (and always wondering how I did not write test first) all of the sudden I realized why sometimes it was easier to develop code with writing tests first and sometimes I just simply did not thought about it and had to write the tests later.

It’s simply because of pair programming.

What certainly happens to me when coding alone, is that sometimes I get into ‘crunch’ mode too fast. With a second person to slow me down, we are discussing the problem and make sure we both have the same understanding of what we are trying to achieve. During this process it is much clearer what needs to be done and what the goals are. This is a very important step. If I’m all by myself, I start writing down some classes, look at methods and I’m already thinking in code and not about the problem. Once you know what the real problem is, it is very easy to start writing your tests first. It helped me a lot.

Where pairing also helps is to focus both programmers to stay in TDD mode. Once the driver of the pair programming couple starts to hack away code, the other will remind him to write the tests first. This is a very valuable.

After a while I got better at test driven development, even programming alone.

So if you recognize that you do not write tests first (but you want to) and you yet again don’t know how that happened, try pair programming. You learn how to start with TDD (and get used to it) and get all the benefits of pair programming as well.