Test your Rule
Authors:
Ankit Mehta, Cille Schliebitz, Anita Gu
Changed on:
4 Feb 2025
Overview
In this module, you will learn about Unit Testing and creating a Mocked Unit test for a custom Rule. You will also learn how to perform testing on the TestExecutor and finally deploy your plugin into your sandbox environment
Key points
- Unit Testing
- PowerMock
- TestExecutor
- Testing on Sandbox
Unit Testing
As Rules are atomic in size and complexity. We recommend a Test-Driven Development (TDD) approach. It is an approach where you first write tests, then use those tests to drive the design and development of your software application/rules. The TDD approach is also known as Red, Green, Refactor:
✅ Write a test (Red)
✅ Get the test to pass (Green).
✅ Optimise the design (Refactor)
Using this approach, Test cases are written for each Rules' functionality and are executed. In the event of a Test failing (I.e. not passing), a new code is written in order to pass the test by making the code simple and bug-free. This approach results in high-quality code.
We recommended that you adopt the following steps when performing testing on the Fluent Platform:
- Create a fully Mocked Unit Test (using IDE's Unit Testing capabilities) for your custom Rule. In the Mocked unit test perform one (1) assertion per test case and test both positive and negative paths.
- Test your Rule in the TestExecutor. The TestExecuter can be viewed as a mini Workflow Engine simulator that runs on your local machine to assert additional behavior.
- Deploy your plugin to your Sandbox account.
Fluent is currently using JUnit 4 as the Unit testing Framework — your plugin project pom.xml already has a dependency on JUnit.
💡 The above steps will be explained in the upcoming lessons of this section
⬇️ Scroll down to reveal more
TestExecutor
The TestExecutor simulates the Workflow Engine and allows developers to set up a mock Workflow for Unit Testing. This testing on TestExecutor helps to assert some additional behaviour of the Rule. Learn more in Testing Rules.
Mocked Unit Testing
Remember this client?
This lesson relates to the Client Scenario — Implement Fraud Check covered in Module 4.
We have already created a Rule MarkOrderHighValueRule that implements Fraud Check, as per our client's requirement. In this lesson, we will create a Mocked Unit Test for the MarkOrderHighValueRule class. These tests are Ideal for TDD (Test Driven Development).
💡 Follow the steps below to create mocked unit tests for your custom GraphQL Rule
Create a Mocked Unit Test class for your custom Rule
- Open your plugin project in which you have written the MarkOrderHighValueRule class.
- In the src/test/java/com.training/rule folder of your plugin project, create a new class named MarkOrderHighValueRuleTest.
- Write a setup method to initiate the mocks. This setup method will run before every test in the class. The setup method uses MockitoAnnotations class to initiate Mocks(initMocks). Mockito is the Fluent mocking framework.
- Define a mock on Context, which is a required parameter of the run method in the Rule class.
Important:
- Make sure you import the v2 version of Context i.e use this package com.fluentretail.rubix.v2.context.Context
- The org.mockito package can mock all classes except final classes.
*After completing Step 1, your code should look similar to the one below:
1
2 package com.training.rule;
3
4 public class MarkOrderHighValueRuleTest
5 {
6
7 private MarkOrderHighValueRule rule = new MarkOrderHighValueRule();
8
9 @Mock
10
11 private Context context;
12
13 @Before
14
15 public void setup()
16 {
17
18 MockitoAnnotations.initMocks(this);
19
20 }
21
22 }
Language: plain_text
Name: MarkOrderHighValueRuleTest
Description:
Test class structure
Use Mockito to write test cases
The Test method uses the AAA (Arrange, Act, and Assert) approach, where you arrange the steps, act and then assert.
Let's create our first test case. We'll begin by asserting the expected behaviour for parameter validation.
The run method of MarkOrderHighValueRule class validates that Rule must have a valid parameter. Given below is the code snippet for validation from MarkOrderHighValueRule class:
1/**
2 * NOTE: This is a snippet from `MarkOrderHighValueRule class from 'Writing the Rules' section
3 **/
4
5// Validation:
6Integer threshold = context.getProp(Constants.PARAM_NAME_HIGH_VALUE_THRESHOLD, Integer.class);
7if (null == threshold)
8{
9 throw new PropertyNotFoundException(400,String.format("Required Parameter not provided: %s", Constants.PARAM_NAME_HIGH_VALUE_THRESHOLD));
10}
Language: plain_text
Name: MarkOrderHighValueRule class validation
Description:
Validate the rule property for threshold - PARAM_NAME_HIGH_VALUE_THRESHOLD
As shown in the above snippet, the run method should throw PropertyNotFoundException if the parameter is null. Our first test case will assert this behaviour of the run method.
Perform the steps below to create a test case:
- Create a test method named run_withMissingThresholdParam_ThrowsPropertyNotFoundException ( ).
- The run method throws a PropertyNotFoundException, so we don't need any assertion in the Test case. The Test expected behaviour will manage the exception with the following line of code: @Test(expected = PropertyNotFoundException.class)
- Use a negative path test: In the arrange section of the Test, get the property. If the property returned is null, then it throws an exception.
- Run the test case: On IntelliJ use control+shift+R key code. Or, right-click inside the method, and select run_withMissingThresholdParam_ThrowsPropertyNotFoundException method.
* Your test case should Pass.
Learn more about AAA approach in the Test Case.

👁🗨 Video Demonstrating Steps 1 & 2: Create a Mocked Unit Test / Use Mockito to write test cases
This video illustrates how to:
✅ Write a Test case.
✅ Import org.mockito package for the Mocking framework.
✅ Import com.fluentretail.rubix.v2.context.Context package for Context.
✅ Write a Test case to assert expected behaviour for parameter validation.
1<dependency>
2 <groupId>org.powermock</groupId>
3 <artifactId>powermock-module-junit4</artifactId>
4 <version>1.6.6</version>
5 <scope>test</scope>
6 </dependency>
7 <dependency>
8 <groupId>org.powermock</groupId>
9 <artifactId>powermock-api-mockito</artifactId>
10 <version>1.6.6</version>
11 <scope>test</scope>
12 </dependency>
Language: plain_text
Name: PowerMock dependencies
Description:
Snippet from the pom.xml about the required dependencies for Powermock
Include PowerMock and prepare to write the test case
As the Event class is a final class, the org.mockito package can't mock it. We need to make use of PowerMock, it will make the final class use a different test runner and will prepare a class for the test.
Perform the steps below to add PowerMock into your plugin project:
- Open the pom.xml file in your project.
- Copy the dependencies ( provided below) under the existing Mockito dependency
- Check the scope is set to test
- Clean the Maven project.
Given below are the dependencies needed for the PowerMock:
⚠️ Make sure your IDE imports Maven changes after the maven's clean.
- As we are using PowerMock, we need to define a different test runner for PowerMock. Refer to the table below to define and prepare the test runner class for PowerMock
Annotation | Purpose |
@RunWith | E.g. @RunWith(PowerMockRunner.class) is used to define a different test runner for PowerMock |
@PrepareForTest | E.g. @PrepareForTest(Event.class) is used to prepare the runner for the Event class as the Event is the final class |
- Define the following Mocks in your testing class
1// Required Mocks
2@Mock
3private Event event;
4@Mock
5private ReadOnlyFluentApiClient apiClient;
6@Mock
7private GetOrderByIdQuery.Data data;
8@Mock
9private GetOrderByIdQuery.OrderById orderById;
10@Mock
11private ActionFactory actionFactory;
Language: plain_text
Name: Required Mocks in testing class
Description:
Required mocked objects
- Define/Write the default behaviour of above defined Mocks in the setup ( ) method.
Click on the + buttons to learn more about the setup() method for Powermock
- PARAM_NAME_HIGH_VALUE_THRESHOLD value - returns the default value of $1000
- (context.getEvent()) - it will return the mocked event value
- event.getEntityId()) - will return the default String value of "123"
- Other context values - context.api() , apiClient.query(any(), data.orderById() will return respective mocked values
👁🗨 Video Demonstrating Step 3: Include PowerMock and prepare to write the test case
This video illustrates how to:
✅ Add PowerMock to your plugin
✅ Define the test runner class and prepare the test class
✅ Define the required Mocks and the default behaviour of defined Mocks in the setup() method.
Create a test case using PowerMock
Let's begin with a simple test case:
"Verify that the Order is updated when the Total Price is above the Threshold"
For this test, we will verify that the MutationAction is queued via the Context's ActionFactory (context.action( ).mutate( )), with the GraphQL Mutation Object ( UpdateOrderAttributesMutation)
Perform the following steps:
- Create a test method named as run_withValueGreaterThanThreshold_queuesUpdateOrderAttributesMutation()
- The "arrange, act and assert" approach of the test method is explained in the given below interaction (See diagram below).
- Run the test case: On IntelliJ use control+shift+R key code. Or, right-click inside the method, and select run run_withValueGreaterThanThreshold_queuesUpdateOrderAttributesMutation() method.
*Your test case should Pass
Click on the + buttons to learn more about the required code for this test case:

- Arrange - if the order's totalprice value is greater than the default value set in the setup() method then it will return value greater than default value for totalPrice. It also returns mocked actionFactory.
- Act - it will make the rule run on the mocked context.
- Assert - It will verify that the mutation is called only one time and is of any UpdateOrder type.
👁🗨 Video Demonstrating Step 4: Create a Test Case using PowerMock
This video illustrates how to:
✅ Write a test case to assert the Order is updated when the Total Price is above the Threshold.
✅ Run the test case using PowerMock defined and prepared runner class
Write another test case using PowerMock
Let us assert that "The Attribute is being built correctly."
We'll start with verifying the attribute name by mocking the AttributeInput.Builder class. This will allow us to verify that the attribute name is set correctly.
Perform the following steps:
- Prepare PowerMockRunner by updating @PrepareForTest annotation to include AttributeInput.class . Your annotation should look like:
@PrepareForTest({Event.class, AttributeInput.class})
- Define a Mock of AttributeInput.Builder by using the below code. This will allow us to verify that the Attribute Name is set correctly.
@Mock
private AttributeInput.Builder mockedAttributeInputBuilder;
- Create a test method named run_withValueGreaterThanThreshold_addsAttributeWithNameIsHighValue() which throws Exception
- Use the given below code for this test case.
- Run the test case.
1@Test
2 public void run_withValueGreaterThanThreshold_addsAttributeWithNameIsHighValue() throws Exception {
3
4 //arrange:
5 when(orderById.totalPrice()).thenReturn(1001.00);
6 when(context.action()).thenReturn(actionFactory);
7
8 AttributeInput attributeInput = AttributeInput.builder().name("test-name").type("test-type").value("test-value").build();
9 PowerMockito.whenNew(AttributeInput.Builder.class).withNoArguments().thenReturn(mockedAttributeInputBuilder);
10 when(mockedAttributeInputBuilder.name(anyString())).thenReturn(mockedAttributeInputBuilder);
11 when(mockedAttributeInputBuilder.type(anyString())).thenReturn(mockedAttributeInputBuilder);
12 when(mockedAttributeInputBuilder.value(any())).thenReturn(mockedAttributeInputBuilder);
13 when(mockedAttributeInputBuilder.build()).thenReturn(attributeInput);
14
15 //act:
16 rule.run(context);
17
18 //assert:
19 verify(mockedAttributeInputBuilder, times(1)).name(Constants.IS_HIGH_VALUE_ORDER_ATTRIBUTE_NAME);
20 }
Language: plain_text
Name: PowerMock Test Case
Description:
Sample method to test if the high-value-attribute exists
👁🗨 Video Demonstrating Step 5: Write a Test Case using PowerMock
This Video illustrates how to:
✅ Write a test case to assert The Attribute is being built correctly
Practice Exercises
Try writing the following test cases in the same project.
- run_withValueGreaterThanThreshold_addsAttributeWithTypeBoolean
- run_withValueGreaterThanThreshold_addsAttributeWithValueTrue
- run_withValueLessThanThreshold_doesNotQueueAnyMutation
- run_withValueLessThanThreshold_doesNotBuildAnyAttribute
- run_withValueEqualThanThreshold_doesNotQueueAnyMutation
- run_withValueEqualThanThreshold_doesNotBuildAnyAttribute
Testing on the TestExecutor
We recommend testing on TestExecutor before you upload your rule into your sandbox account. TestExecutor shows how the rule behaves when running in a local ThreadExecutor. It also asserts some additional behaviour.
The TestExecutor allows developers to setup a mock workflow for unit testing of specific rules and simulates the Workflow Framework and provides the following methods:
- rule(Class.class): load a rule
- ruleset(RuleSet ruleSet): adds a ruleset to the workflow
- entity(Entity entity): adds an entity to the workflow
- validateWorkflow(Event event): validates the workflow with the given event
- execute(Event event): executes the workflow with the given event
After execution, your tests can use the following for assertions:
- count*(): Provides access to the number of events or actions that have been executed in the context.
- get*(): Provides access to the entities, rules, events, and properties.
In this section, we will test our Rule in the TestExecutor. We will run the Rule under the following conditions, and assert the expected outcomes:
- run_withTotalPriceGreaterThanThreshold_callsMutateAction
- run_withTotalPriceLessThanThreshold_doesNotCallMutateAction
- run_withTotalPriceGreaterThanThreshold_doesNotCallMutateAction
Step 1 - Create a test class
In the src/test/java/com.training/rule folder of your plugin project, create a new class named MarkOrderHighValueRuleExecutorTest .
Declare fields, mocks, and setup() method as given in the code snippet below.
1public class MarkOrderHighValueRuleExecutorTest {
2
3 private static final Integer HIGH_VALUE_THRESHOLD = 1000;
4 private static final String ORDER_ID = "123";
5
6 private TestExecutor executor;
7
8 private Event event;
9
10 @Mock
11 private ReadOnlyFluentApiClient apiClient;
12
13 @Mock
14 private GetOrderByIdQuery.Data data;
15
16 @Before
17 public void setup() {
18
19 MockitoAnnotations.initMocks(this);
20
21 when(apiClient.query(any())).thenReturn(data);
22 }
23}
Language: plain_text
Name: MarkOrderHighValueRuleExecutorTest
Description:
Unit Test class with TestExecutor setup
Step 2 - Create an Event entity
In the setup method, create an instance of a RubixEntity. This is an orchestratable entity inside of Rubix, which is made available to the Rule via the Context.
Given below is the code snippet to create an entity
1@Before
2 public void setup() {
3
4 MockitoAnnotations.initMocks(this);
5
6 when(apiClient.query(any())).thenReturn(data);
7
8 // Setup up an instance of a Rubix Entity
9 RubixEntity entity = RubixEntity.builder()
10 .status("BOOKED")
11 .entityType("ORDER")
12 .flexType("flexType")
13 .flexVersion(1)
14 .ref(ORDER_ID)
15 .id(UUID.randomUUID().toString())
16 .build();
17
18 }
Language: plain_text
Name: MockitoAnnotations
Description:
Create Rubix Entity with the required fields set
Step 3 - Create an Event to trigger the Ruleset
In the setup method, prepare the Event that will be used to trigger the workflow. Refer to the below interaction, click on + buttons to learn more about the code

- Event.builder on declared Event instance - Use Event builder to build the event with the attributes of retailerID, entityID, rootEntityRef, etc.
- Event.builder.build - Once the event is built, it will be used to trigger the workflow.
👁🗨 Video Demonstration for Steps 1 - 3
Click the Play Button ▶️ to begin the video.
This video illustrates how to:
✅ Write a test class
✅ Create an Event entity, and an Event to trigger the Ruleset
Step 4 - Create the Rule instance and Ruleset
We need to provide the following to TestExecutor to execute the test:
- The Rule we are going to test.
- Setup the Ruleset- ruleset configured with an instance of the Rule, an Entity, and an Event.
Do the following to create a Ruleset:
- Create a map in the setup() method to show how the Rule is configured with Rule parameters in the Ruleset.
- Create an instance of a Ruleset in the setup() method to hold an instance of the Rule that is configured with the parameters as shown in the interaction below

- map of rule parameters - ImmutableMap will map required parameters of the Rule.
- event paramater of ruleset - It is the event that is triggering the ruleset.
- name of the rule - MarkOrderHighValueRule.class.getSimpleName() is the name of the Rule class
- map builder - propertyBuilder.build() is the map builder that states how the rule is configured
Step 5 - Setup the TestExecutor
In the setup() method, initialise the TestExecutor to simulate the Workflow Engine using the code snippet below:
1// Set up a TestExecutor to simulate Orchestration Engine
2 executor = TestExecutor.builder()
3 .rule(MarkOrderHighValueRule.class)
4 .ruleset(ruleSet)
5 .entity(entity)
6 .testApiClient(apiClient)
7 .build();
Language: plain_text
Name: Initialise TestExecutor
Description:
Build the TestExecutor object
Step 6 - Create a test case to run on TestExecutor
We have our TestExecutor prepared. Follow the steps to implement the test case.
- Create a test method named run_withTotalPriceGreaterThanThreshold_callsMutateAction
- The test method "arrange, act and assert" approach is in the below-given code snippet.
- Run the test case.
1@Test
2 public void run_withTotalPriceGreaterThanThreshold_callsMutateAction() {
3
4 // arrange:
5 GetOrderByIdQuery.OrderById orderById = GetOrderByIdQuery.OrderById.builder().id(ORDER_ID).totalPrice(1001.00).type("type").__typename("type-name").build();
6
7 when(data.orderById()).thenReturn(orderById);
8
9 executor.validateWorkflow(event);
10
11 // act:
12 TestContext context = executor.execute(event);
13
14 // assert:
15 assertEquals(1, context.countActionsOfType(TestContext.MutateAction.class));
16 }
Language: plain_text
Name: AAA approach (arrange, act, and assert)
Description:
Sample Test method to execute the UnitTest
👁🗨 Video Demonstration for Steps 4 - 6
This Video illustrates how:
✅ Create Rule instance and Ruleset
✅ Setup a TestExecutor
✅ Write a test case to run on TestExecutor
Practice Exercises
Try writing the following test cases in the same project.
- run_withTotalPriceLessThanThreshold_doesNotCallMutateAction
- run_withTotalPriceEqualToThreshold_doesNotCallMutateA
Testing on Sandbox
Once you've got good coverage of your Rules in Unit Tests and using the TestExecutor locally, you can deploy your plugin to your sandbox account.