Testing business rules

Creating test data

The GroovyBusinessRuleExecutor class provides functionality for using contexts for testing business rules. We have the following contexts available through GroovyExecutionScope inside rule.groovy:

CHANNEL_PRODUCT, STATUS, CATEGORY, HIERARCHY, PACKAGING, FAMILY

The general syntax for using the GroovyBusinessRuleExecutor is:

GroovyBusinessRuleExecutor.given(businessRule)
  .withProduct(channelProduct)            //optional
  .withAttributes(attributes)             //optional
  .withStatusContext(statusContext)       //optional
  .withPackagingContext(packagingContext) //optional
  .withCategoryContext(categoryContext)   //optional
  .withHierarchyContext(hierarchyContext) //optional
  .withFamilyContext(withFamilyContext)   //optional
.isExecuted();

In the following sections we will discuss how to add different contexts using this pattern.

Channel product

To build a contextMap containing CHANNEL_PRODUCT, we need to understand how a product following the channel data standard is represented in a JSON-like manner inside the channel. This is based on the following concepts:

Attribute TypeDescription
TestAttribute.SingleThis is a single value encoded as a string:

• The number 1 must be encoded as "1"
• The boolean value true must be encoded as "true"

"single": "value"
TestAttribute.CompositeA composite is a map of ID => attribute, and is similar to a JSON-object:

"compositeAttrId": {
"nestedA": "value A",
"nestedB": "value B",
...
}
TestAttribute.MultiA multi valued attribute can contain a list of attribute values, and is similar to a JSON-array:

"multiAttrId": [ "value1", "value2", ... ]

Example 1 (Multi with Composites with Singles)

Here is a JSON encoding of the structure we want to build:

{
    "multiAttrId": [
        {
            "a": "first",
            "b": "second"
        },
        {
            "a": "third",
            "b": "fourth"        
        }
    ]
}

This is a channel product that contains:

  • A multi valued attribute with ID=”multiAttrId”
  • Two composite values with nested attributes “a” and “b”

📘

In PDX, a Multi will have elements that are copies of the same attribute in the data standard. This is why both Composite copies have the same nested attributes “a” and “b” in the example above.

This can be built using the following snippet:

ChannelProduct product = new TestChannelProductBuilder(
        new TestAttribute.Multi(
            "multiAttrId",
            new TestAttribute.Composite(
                new TestAttribute.Single("a", "first"),
                new TestAttribute.Single("b", "second")
            ),
            new TestAttribute.Composite(
                new TestAttribute.Single("a", "third"),
                new TestAttribute.Single("b", "fourth")
            )
        )
    )
    .build();

The result of this is that we now have access to this data via the contextMap. Here is an example in Java:

var product = ((ChannelProductContext) contextMap.get("CHANNEL_PRODUCT")).getProduct();  
var multi = product.getAttribute("multiAttrId");  
var itA = multi.getMultiValue("a");  
List<String> sliceA = new ArrayList\<>();  
while (itA.hasNext()) sliceA.add(itA.next().getValue());

which results in a slice through all the “a” attributes inside the Composites in the Multi:

sliceA = [ "first", "third" ]

Example 2 (Composite with a Single and a Multi)

Here is a JSON encoding of the structure we want to build:

{
    "compositeId": {
        "singleAttrId": "value",
        "multiAttrId": [
            "element1",
            "element2"
        ]
    }
}

This is a channel product that contains:

  • A composite attribute with ID=”compositeId” with a single valued and a multi valued nested attribute
  • A single valued attribute with ID=”singleAttrId”
  • A multi valued attribute with ID=”multiAttrId”

This can be built using the following snippet:

ChannelProduct product = new TestChannelProductBuilder(
        new TestAttribute.Composite(
            "compositeId",
            new TestAttribute.Single("singleId", "value"),
            new TestAttribute.Multi(
                "multiId",
                new TestAttribute.Single("element1"),
                new TestAttribute.Single("element2")
            )
        )
    )
    .build();

In Java we can now access this data like for instance:

var product = ((ChannelProductContext) contextMap.get("CHANNEL_PRODUCT")).getProduct();  
var composite = product.getAttribute("compositeId");  
String single = composite.getValue("singleId").getValue();  
Iterator<String> multiIterator = composite.getValue("multiId").getMultiValue();  
List<String> multi = new ArrayList\<>();  
while (multiIterator.hasNext()) multi.add(multiIterator.next());

the results are:

singleId = "value"  
multiId = [ "element1", "element2" ]

Testing rule execution

The current test setup for rules uses the following executor:

GroovyBusinessRuleExecutor.given(businessRule).withProduct(channelProduct).isExecuted();

which has the following parameters:

ParameterDescription
BusinessRule businessRuleThe business rule object built using one of the builders described in the Rules Types sections above
ChannelProduct channelProductThe product built using the TestChannelProductBuilder described in Creating Test Data

Additional contexts can be provided using their respective ‘with’ methods.

Full example

To summarize, this section will compile a minimal example using the full setup for creating, building and running a test execution of a groovy rule:

The file rule.groovy looks like this:

package groovyRules;

import static com.stibo.leap.datastandard.extractor.sdk.businessrules.BusinessRuleUtils.*;
import com.stibo.leap.datastandard.extractor.sdk.businessrules.GroovyExecutionScope;

def product = GroovyExecutionScope.CHANNEL_PRODUCT?.getProduct();
def multi = product?.getAttribute("multiId").getMultiValue();

multi.each {element -> if (nullSafeEquals("test", element)) {
    GroovyExecutionScope.errors
        .put(
            "multiId", 
            "The list of values cannot contain the string \"test\""
        )
}};

return !GroovyExecutionScope.errors.isEmpty();

and we can build and test it like this:

@Test
public void shouldBuildAndRunRuleDotGroovy() throws IOException {
    BusinessRule rule = BusinessRuleBuilder.groovyRule("RULE_ID")
        .withGroovyCode(new File("rule.groovy"))
        .build();
    
    ChannelProduct product = new TestChannelProductBuilder(new TestAttribute.Multi(
                "multi",
                new TestAttribute.Single(null),
                new TestAttribute.Single("test")
            )).build();
        
    GroovyBusinessRuleExecutor.given(rule).withProduct(product).isExecuted()
            .failed()
            .errorMap().containsExactly(entry("multi", "The list of values cannot contain the string \"test\""));
}