As most users know, Drools offers a user friendly web based scenario testing tool in Guvnor. Sometimes, though, the rules are developed by the technical team and the team might have a testing process in place using standard xUnit frameworks.
The good news is that testing rules with your favorite xUnit framework is simple and no different from testing any other piece of code. This quick tutorial presents simple recipes to test rules using:
* JUnit 4 – www.junit.org
* Mockito – www.mockito.org
This tutorial assumes familiarity with the above frameworks. Please consult the appropriate documentation if you are not familiar with them. The same approach can be used with any xUnit framework and with any mocking framework.
During this tutorial we will test these 2 simple rules:
package com.sample
rule "Name is Bob"
salience 10
when
$p : Person( name == "Bob", $age : age )
then
insert( "Bob is "+$age+" years old." );
end
rule "Person is 35 years old"
salience 5
when
Person( age == 35, $name : name )
then
insert( "Person is 35 years old" );
end
Before and after each test, we have to create the knowledge base and load the rules. In JUnit we can use the @Before and @After annotations to create setup and teardown methods:
@Before
public void setup() {
try {
// load up the knowledge base
kbase = readKnowledgeBase();
// create the session
ksession = kbase.newStatefulKnowledgeSession();
} catch ( Exception e ) {
e.printStackTrace();
Assert.fail( e.getMessage() );
}
}
@After
public void tearDown() {
if ( ksession != null ) {
ksession.dispose();
}
}
Each unit test, has to follow basically 3 steps:
1. Prepare the listeners
2. Execute the scenario
3. Verify the scenario results
In the next few sections, we will detail a few ways to test different aspects of the rules execution.
R1. How to check that the rules execute and no exception is raised
An easy way to check that the rules execute is to test the return value of the fire all rules. So to check that for a given fact, both rules execute and no exception is raised, we just need to create a unit test:
@Test
public void testRulesFire() throws Exception {
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
int rulesFired = ksession.fireAllRules( );
// ***********************************************************
// verify the results
// ***********************************************************
Assert.assertEquals( 2, rulesFired );
}
R2. How to prevent infinite loops in tests
To prevent infinite loops during testing, an easy way is to simply use the limit parameter on fire all rules. The engine will pause execution and return the control to the test once all rules have fired or the limit was reached. E.g.:
@Test
public void testRulesFire2() throws Exception {
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
// do not fire more than 10 rules
int rulesFired = ksession.fireAllRules( 10 );
// ***********************************************************
// verify the results
// ***********************************************************
Assert.assertEquals( 2, rulesFired );
}
R3. How to test Individual Rules
The problem with previous tests is that the initial data will fire both rules. Sometimes it is necessary to test individual rules. To test individual rules, we need to prevent any other rule from firing. To allow that, we can use Agenda Filters. The AgendaFilter is a one-method interface that the user can implement or he can use one of the implementations that come with Drools.
Drools ships with several implementations, including some that check the rule name to allow or prevent rules from firing. For instance, if we want to test the rule “Name is Bob” individually, all we need to do is to use an appropriate agenda filter when firing the rules:
@Test
public void testBobRuleOnly() throws Exception {
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
// we only want to fire the Bob rule, so use an agenda filter for that
int rulesFired = ksession.fireAllRules( new org.drools.base.RuleNameEqualsAgendaFilter( "Name is Bob" ) );
// ***********************************************************
// verify the results
// ***********************************************************
Assert.assertEquals( 1, rulesFired );
}
To know more about AgendaFilters, check the Drools javadoc documentation for org.drools.runtime.rule.AgendaFilter and its implementations.
R4. How to check executed rules
Unfortunately, just checking the number of executed rules is rarely enough to make sure the rules are correct. Many times, the test must make sure that the rules executing are the ones that should be executing and that they are doing so in the expected sequence.
Fortunately, Drools offers a listener framework that allows the application (and the tests) to be notified about every single step the engine is taking. Combine that with a mocking framework like Mockito and you have a really powerful and flexible testing infrastructure.
There are several different listeners, but the 2 most frequently used are the AgendaEventListener (that listens for agenda changes like rules being activated, fired, cancelled, etc) and the WorkingMemoryEventListener (that listens for changes on the working memory, like facts being inserted, modified or retracted). For a list of all listeners and their methods, please consult the Drools javadoc documentation and look for org.drools.event.rule.AgendaEventListener, org.drools.event.rule.WorkingMemoryEventListener, org.drools.event.process.ProcessEventListener and org.drools.event.knowledgebase.KnowledgeBaseEventListener.
To test that 2 rules are fired using the listener framework, on can use Mockito to create mock listeners, like the mock AgendaEventListener bellow, and check that the afterActivationFired method was called twice.
@Test
public void testRulesFired() throws Exception {
// ***********************************************************
// create the mock listeners and add them to the session
// ***********************************************************
// AgendaEventListeners allow one to monitor and check rules that activate, fire, etc
AgendaEventListener ael = mock( AgendaEventListener.class );
ksession.addEventListener( ael );
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
ksession.fireAllRules( 10 );
// ***********************************************************
// verify the results
// ***********************************************************
verify( ael, times(2) ).afterActivationFired( any( AfterActivationFiredEvent.class ) );
}
More than that, using Mockito’s capture feature, it is possible to capture the exact event instances and verify values, like the rule names that were fired. For more details on how to capture parameters, please consult the Mockito documentation.
@Test
public void testRulesFiredInSequence() throws Exception {
// ***********************************************************
// create the mock listeners and add them to the session
// ***********************************************************
// AgendaEventListeners allow one to monitor and check rules that activate, fire, etc
AgendaEventListener ael = mock( AgendaEventListener.class );
ksession.addEventListener( ael );
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
ksession.fireAllRules( 10 );
// ***********************************************************
// verify the results
// ***********************************************************
// create and argument captor for AfterActivationFiredEvent
ArgumentCaptor<AfterActivationFiredEvent> aafe = ArgumentCaptor.forClass( AfterActivationFiredEvent.class );
// check that the method was called twice and capture the arguments
verify( ael, times(2) ).afterActivationFired( aafe.capture() );
List<AfterActivationFiredEvent> events = aafe.getAllValues();
// check the rule name for the first rule to fire
AfterActivationFiredEvent first = events.get( 0 );
Assert.assertThat( first.getActivation().getRule().getName(),
is("Name is Bob") );
// check the rule name of the second rule to fire
AfterActivationFiredEvent second = events.get( 1 );
Assert.assertThat( second.getActivation().getRule().getName(),
is("Person is 35 years old") );
}
R5. How to check working memory changes
The same way that it is possible to check rules, we can also check changes to the working memory using a mock WorkingMemoryEventListener. The first rule inserts in its consequence a new fact into the working memory. We can check the inserted fact like this:
@Test
public void testWorkingMemoryChanges() throws Exception {
// ***********************************************************
// create the mock listeners and add them to the session
// ***********************************************************
// WorkingMemoryEventListeners allow the test to check facts that are inserted, modified, retracted
WorkingMemoryEventListener wmel = mock( WorkingMemoryEventListener.class );
ksession.addEventListener( wmel );
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
ksession.fireAllRules( new RuleNameEqualsAgendaFilter("Person is 35 years old") );
// ***********************************************************
// verify the results
// ***********************************************************
// create and argument captor for ObjectInsertedEvent
ArgumentCaptor<ObjectInsertedEvent> oie = ArgumentCaptor.forClass( ObjectInsertedEvent.class );
// check that the method was called twice: once for the "bob" fact,
// and a second time for the fact inserted by the rule
verify( wmel, times(2) ).objectInserted( oie.capture() );
List<ObjectInsertedEvent> events = oie.getAllValues();
// check the fact that was inserted by the test
ObjectInsertedEvent first = events.get( 0 );
Assert.assertThat( (Person) first.getObject(),
is( bob ) );
// check the fact that was inserted by the rule
ObjectInsertedEvent second = events.get( 1 );
Assert.assertThat( (String) second.getObject(),
is("Person is 35 years old" ) );
}
Please note that we added the mock working memory listener before inserting the facts into the working memory, so the Person fact (bob) was also captured by the lisneter. If you don’t want to capture the facts that were inserted by the test, you can add the listener after seeding the working memory with the initial dataset, or you can reset the listener after doing it.
R6. How to check bound values on rules
Finally, for our last recipe, what happens if it is necessary to check the value of a given bound variable in a rule? Easily enough, we do the same we have been doing, using the agenda listener. Also, we can use MVEL to evaluate arbitrary expressions and check the result of the expression.
@Test
public void testCheckBindingValues() throws Exception {
// ***********************************************************
// create the mock listeners and add them to the session
// ***********************************************************
// AgendaEventListeners allow one to monitor and check rules that activate, fire, etc
AgendaEventListener ael = mock( AgendaEventListener.class );
ksession.addEventListener( ael );
// ***********************************************************
// execute the scenario to be tested
// ***********************************************************
// insert the required data
Person bob = new Person( "Bob",
35 );
ksession.insert( bob );
ksession.fireAllRules( new RuleNameEqualsAgendaFilter("Name is Bob") );
// ***********************************************************
// verify the results
// ***********************************************************
// create and argument captor for AfterActivationFiredEvent
ArgumentCaptor<AfterActivationFiredEvent> aafe = ArgumentCaptor.forClass( AfterActivationFiredEvent.class );
// check that the method was called twice and capture the arguments
verify( ael ).afterActivationFired( aafe.capture() );
// check the rule name for the first rule to fire
AfterActivationFiredEvent first = aafe.getValue();
Assert.assertThat( first.getActivation().getRule().getName(),
is("Name is Bob") );
// check value of $age
Assert.assertThat( (Integer) first.getActivation().getDeclarationValue( "$age" ),
is( 35 ) );
// check the value of an arbitrary expression using MVEL
Person $p = (Person) first.getActivation().getDeclarationValue( "$p" );
Integer age = (Integer) MVEL.eval( "$p.age", Collections.singletonMap( "$p", $p ) );
Assert.assertThat( age, is( 35 ) );
}