The articles below will walk you through the library of utility functions designed to simplify Rules testing. It provides methods to interact with Rules in isolation, making it easier to test various scenarios. These Utilities are commonly used in conjunction with JUnit. Prerequisites
The `util-test` library is a comprehensive collection of utility functions, mock objects, and executors designed to minimize the overhead and complexity of testing your Fluent Commerce rules.PrerequisitesThese articles assumes you're familiar with:
Java
Maven
JUnit
Key points
`RuleExecutor`: The primary tool for unit testing a single rule in isolation.
`WorkflowExecutor`: An advanced executor for integration testing an entire workflow from a JSON definition.
`RuleContextGenerator`: The underlying builder for creating the mock `Context`, allowing you to define the event, entity, properties, and mock API responses for your test.
`MockApiClient`: A critical component for providing predefined GraphQL responses, enabling you to test how your rule handles different API outcomes without making live calls.
`TestActions`: A helper object that captures all actions your rule performs and provides fluent assertions (e.g., `sendEvent().assertCount(1)`) to easily verify the results.
Java Docs
The helper methods contained in the Utility Bundles libraries are many and varied, so the JavaDocs provided with the packages should be treated as the source of truth for documentation.
Value Proposition
Test Logic, Not the Platform: The utilities allow you to focus your tests on your rule's specific business logic, rather than on the intricacies of the platform's execution environment.
Fast and Isolated Tests: Because the tests run entirely in-memory with a mock environment, they are extremely fast and can be run as part of any standard CI/CD pipeline without needing a live Fluent instance.
Readable and Maintainable Tests: The builder-style pattern (e.g., `RuleExecutor.of(...).withProp(...).withEvent(...)`) leads to tests that are easy to read and understand, following the popular Arrange-Act-Assert pattern.
Full Coverage: With tools to mock events, entities, rule properties, and API calls, you can create test cases for both success and failure scenarios, helping ensure your rules behave correctly under various conditions.
Explanation through an Example
The following Rule was written to send an Event with a specified name when a Property of a `RubixEntity` is less than a given value.
1packagecom.fluentcommerce.rule.flow;23importcom.fasterxml.jackson.databind.JsonNode;4importcom.fluentcommerce.util.core.JsonUtils;5importcom.fluentretail.rubix.rule.meta.EventInfo;6importcom.fluentretail.rubix.rule.meta.ParamString;7importcom.fluentretail.rubix.rule.meta.RuleInfo;8importcom.fluentretail.rubix.v2.context.Context;9importcom.fluentretail.rubix.v2.rule.Rule;10importcom.google.common.collect.ImmutableList;1112importstaticcom.fluentcommerce.util.core.EventUtils.forwardInboundEventWithNewName;13importstaticcom.fluentcommerce.util.core.LogUtils.logOnce;14importstaticcom.fluentcommerce.util.dynamic.DynamicUtils.query;1516@RuleInfo(name ="IfPropertyIsLessThan", description ="If {jsonpath} is less than {value}, do {eventName}",17 produces ={@EventInfo(eventName ="{eventName}")})18@ParamString(name ="jsonpath", description ="Property of the entity, e.g. 'fulfilmentChoice.deliveryType'")19@ParamString(name ="value", description ="Value to compare, e.g. 'EXPRESS'")20@ParamString(name ="eventName", description ="Name of the event to send")21publicclassIfPropertyIsLessThanimplementsRule{22@Override23publicvoidrun(Context context){24String jsonPath = context.getProp("jsonpath");25String value = context.getProp("value");2627JsonNode result =query(context,ImmutableList.of(jsonPath)).get(jsonPath);2829if(JsonUtils.marshallAndCompare(value, result)>0){30forwardInboundEventWithNewName(context, context.getProp("eventName"));31}else{32logOnce(context,IfPropertyIsLessThan.class,"Values '%s' is not less than `%s`", result, value);33}34}35}36
Here is a collection of common scenarios for the Test Utility methods usage:
Retrieving a single variable from an Apollo mutation as a POJO
To retrieve a single variable from an Apollo mutation as a POJO for test assertions, use the `ApolloUtils.getMutationVariable` method:
1@AllArgsConstructor2publicstaticclassUpdateInput{3String id;4Double totalPrice;5}6// get variable from mutation input7Double totalPrice =ApolloUtils.getMutationVariable(mutation,"totalPrice",Double.class);
This is particularly useful in executor-based tests, as it’s often the only variable in the mutation, making it easy to assert outcomes.
Retrieving multiple variables from an Apollo mutation as a POJO
To retrieve variables from an Apollo mutation as a POJO for test assertions, use the `ApolloUtils.getMutationVariables` method:
1@AllArgsConstructor2publicstaticclassUpdateInput{3String id;4Double totalPrice;5}6// get variable from mutation input7UpdateInput input =ApolloUtils.getMutationVariables(mutation,UpdateInput.class);
Retrieving variables from an Apollo mutation as a JsonNode
Alternatively, you can retrieve the variables from an Apollo mutation as a `JsonNode` for test assertions, using the `ApolloUtils.getMutationVariables` method:
1JsonNode node =getMutationVariables(mutation);
Getting the Class object for all the Rules of the project
To build the `RuleRepository` before test executions or check rule metadata (such as enforcing naming standards), use the `TestUtils.gatherRules` method:
1TestUtils.gatherRules()
Creating an Event with default values
To create an event with default values for unit tests, use the `TestUtils.eventWithDefaults` method:
1finalEvent event =TestUtils.eventWithDefaults()
Creating an Event with some sensible default values
To create an event with sensible default values for unit tests, use the `TestUtils.eventWithDefaults` method.
Particularly, when working with complex systems like Rule-based engines or Workflows. Executors help to make testing more efficient, reliable, and maintainable by:
Managing the flow of test execution
Ensuring isolation
And handling dependencies
Note
Find more details about the available Executors with the Related content links below.
The `RuleContextGenerator` is the underlying class that the `RuleExecutor` and `WorkflowExecutor` use to construct a mock `Context` for your tests. While you will most often interact with it through the executors, you can also use it directly to manually create a `Context` object.This is particularly useful when you are unit testing a helper or utility class that requires a `Context` object as a parameter, but you don't need to execute a full rule.
Rule Executor
Author:
Kirill Gaiduk
Changed on:
5 Sept 2025
Overview
The `RuleExecutor` is the primary tool in the `util-test` library for unit testing a single rule in isolation. It provides a builder-style interface for setting up a mock `Context`, executing your rule's `run` method, and asserting the results.
Key points
Unit Testing Focus: `RuleExecutor` is the main tool for testing a single rule in isolation.
Arrange-Act-Assert: It's designed to follow the clean Arrange-Act-Assert pattern. You use the constructor to arrange the context, call `execute()` to act, and use `getActions()` to assert the outcome.
Rule Properties Configuration: Rule properties are passed as a `Map` in the `RuleExecutor.of(Class, Map)` constructor.
Event Configuration: Custom events can be passed to the `execute(Event)` method.
GraphQL Mocking: The `.mockNamedQuery()` method is a critical feature that allows you to provide predefined JSON responses for any GraphQL query your rule executes, enabling you to test different data scenarios without making live API calls.
Arrange-Act-Assert
Testing with `RuleExecutor` follows the standard Arrange-Act-Assert pattern, which makes tests clean and easy to understand by clearly separating each phase:
Arrange (Build the Context)
This is the setup phase where you define all the conditions for your test case. You start by calling `RuleExecutor.of(MyRule.class, Map)` with rule properties.
1// ARRANGE: Set up the rule executor with properties and a mock query2RuleExecutor executor =RuleExecutor.of(MyRuleToTest.class,ImmutableMap.of(3"eventName","success-event"4))5.mockNamedQuery(6GetOrderQuery.class,7"graphql/getOrderResult.json"8);910Event event =Event.builder()11.name("TestEvent")12.entityRef("order-123")13.entityType("ORDER")14.attributes(ImmutableMap.of(15"eventAttributeKey","eventAttributeValue"16))17.build();
Act (Execute the Rule)
This is the simplest step. The `execute()` method runs your rule's `run` method using the mock context you just configured.You can pass a custom event if needed. You can pass a custom event if needed.
After execution, you can retrieve the actions that your rule performed and use the built-in fluent assertions to verify them.
1// ASSERT2// Verify exactly one event was sent3assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(),1);45// Verify the event name6assertEquals(7 context.getLastActionOfType(TestActions.SendEventAction.class).getEvent().getName(),8"success-event"9);1011// Verify no other actions were performed12assertEquals(context.getActionsOfType(TestActions.MutateAction.class).size(),0);13assertEquals(context.getActionsOfType(TestActions.WebhookAction.class).size(),0);
Core Methods
`of` (ruleName, props)
To create a `RuleExecutor` for a rule specified by its name, use the `RuleExecutor.of` method passing the specific name of the Rule.
If you need to execute a Rule with an Event containing attributes or entity details, use the `RuleExecutor` to pass in the customized Event. Any null fields will be auto-populated with values referencing an Orderentity.
Use the `RuleExecutor.resetApi` method to create a new API mock and clear any previously mocked queries. This allows you to start each test with a clean slate and ensures that previous mocks do not interfere with the current test execution.
1RuleExecutor ruleExecutor = executor.resetApi()
`replaceApi`
To replace the executor’s API mock with your own MockApiClient, use the `RuleExecutor.replaceApi` method. This allows you to reuse the same mock configuration across multiple tests, ensuring consistency and reducing setup overhead.
1finalMockApiClient api =newMockApiClient();2RuleExecutor ruleExecutor = executor.replaceApi(api)
`withLegacyClient`
To use the provided legacy client instead of a stubbed one, utilize the `RuleExecutor.withLegacyClient` method. This enables you to connect to a real client for more accurate testing or simulate a specific client setup during the execution.
While the `RuleExecutor` is perfect for unit testing a single Rule, the `WorkflowExecutor` is the tool of choice for integration testing an entire Workflow or parts of it. It reads a Workflow definition from a JSON file and simulates the Fluent Orchestration Engine by executing the sequence of Rules and Rulesets.
Key points
Integration Testing Focus: `WorkflowExecutor` is the primary tool for integration testing an entire or parts of a Workflow.
Workflow Simulation: It mimics the Fluent Commerce Orchestration Engine by processing an initial Event and then passing the resulting Event to the next Ruleset in the chain.
Rule Mocking: The `.mockRule()` method is a powerful feature that allows you to replace a real Rule in the Workflow with a mock implementation. This is crucial for isolating the part of the Workflow you want to test.
Targeted API Mocking: The `.mockNamedQuery()` method lets you mock a GraphQL query only when it is called by a specific Rule, giving you fine-grained control over test data for different stages of the Workflow.
Consolidated Assertions: The `execute` method returns a single Context containing a consolidated list of all Actions from all Rules, allowing you to assert the final state.
Core Methods
The `WorkflowExecutor` builds upon the capabilities of the previous `TestExecutor` and adds several new features:
`of`
This method automatically loads Workflows from a JSON file and includes Rules within the project.
The `WorkflowExecutor` allows you to simulate Rule executions by specifying mock behaviors for Rules based on their names, bypassing the need for actual Rule execution.In other words, you can instruct the `WorkflowExecutor`: “Whenever a Rule with this name is encountered, skip the actual execution and simply assume it produced this Action.”
1workflowExecutor.mockRule("SendEvent", c -> c.v2().action().sendEvent(2Event.builder().name((String) c.getRule().getProps().get("eventName")).build()3));
`mockNamedQuery`
In order to define specific mock API responses for each Rule identified by name you can use this method.
Use the `WorkflowExecutor.execute` method to execute the workflow and return the `TestContext` with any produced actions. This method runs the workflow with a default event that has no attributes and references an orderentity.
Use the `WorkflowExecutor.execute` method with an event parameter to execute the workflow with a specific Event and return the `TestContext` with any actions that were produced. This version allows you to run the rule with custom event attributes and/or entity details.
Testing with `WorkflowExecutor` is similar to the `RuleExecutor` but is designed to test the interactions between rules.
1. Test Workflow
First, you need a workflow definition file in your `src/test/resources` directory (e.g., `workflows/my_order_workflow.json`). This file defines the sequence of rules that are triggered by specific events.
The test class uses the `WorkflowExecutor` to load the workflow, mock any necessary dependencies, and execute it with an initial event.
1importcom.fluentcommerce.util.test.executor.WorkflowExecutor;2importcom.fluentcommerce.util.test.executor.RuleContextGenerator;3importcom.fluentretail.rubix.event.Event;4importorg.junit.jupiter.api.Test;56publicclassMyOrderWorkflowTest{78@Test9voidtest_FullOrderWorkflow(){10// 1. Arrange: Load the workflow file11WorkflowExecutor executor =WorkflowExecutor.of("workflows/my_order_workflow.json")12// Mock a GraphQL query for a specific rule in the workflow13.mockNamedQuery(14"TEST.custom.ValidateOrder",15GetOrderByIdQuery.class,16"data/order_to_validate.json"17)18// Mock a rule entirely. When SendOrderWebhookEvent is called,19// just log a message instead of running the real rule.20.mockRule(21"TEST.custom.SendOrderWebhookEvent",22 context -> context.context().log("SendOrderWebhookEvent rule was called")23);2425// 2. Act: Execute the workflow with an initial event26Event initialEvent =Event.builder()27.name("CREATE")28.entityRef("order-123")29.build();30RuleContextGenerator context = executor.execute(initialEvent);3132// 3. Assert: Verify the final state33// Check the sequence of events that were created34assertEquals(1, context.getActionsOfType(SendEventAction.class).size());35assertEquals("RunSourcingLogic", context.getLastActionOfType(SendEventAction.class).getEvent().getName());3637// Check that the mocked rule's log message exists38assertEquals(1, context.getActionsOfType(LogAction.class).size());39assertTrue(context.getLastActionOfType(LogAction.class).getMessage().contains("SendOrderWebhookEvent rule was called"));40}41}
The `MockApiClient` is a critical component of the `util-test` library. It provides a mock implementation of the `ApiClient`, which is the interface Rules use to communicate with the Fluent Commerce GraphQL API.By using the `MockApiClient`, you can test how your Rule behaves with different API responses without making any actual network calls. This isolates your tests, makes them faster, and allows you to simulate any scenario, including errors or empty data results.
Key points
Test API Interactions: The `MockApiClient` allows you to test how your rule responds to different API outcomes (e.g., success, failure, empty data) without making live network calls.
Isolate and Speed Up Tests: By mocking the API, your tests run faster and are isolated from network or environment issues.
Executor Integration: You typically don't use `MockApiClient` directly. You use the helper methods on `RuleExecutor` and `WorkflowExecutor` (like `.mockQuery()` and `.mockSetting()`) to configure it.
Full Coverage: It supports mocking pre-compiled queries/mutations, dynamic queries/mutations, and settings, giving you complete control over your test environment.
How It Works
You typically don't interact with the `MockApiClient` directly. Instead, you use the helper methods on the `RuleExecutor` or `WorkflowExecutor`, such as `.mockQuery()`, which configure the underlying mock client for you.When your rule calls `context.api().query(...)` or `context.api().mutation(...)`, the mock client intercepts the call. Instead of making a network request, it looks for a predefined response that you have provided for that specific query or mutation. If it finds one, it returns the mock response; otherwise, it will typically return an empty result.By default, the `RuleExecutor` and `WorkflowExecutor` automatically create a mock API client that the Rules under test use to perform queries.However, if you need more control over the API behavior, you can manually create and configure a `MockApiClient` to simulate specific responses or conditions during your test.
1MockApiClient api =newMockApiClient();2// now mock any responses to `api.query(GetSettingsQuery.builder().build())`3api.mockNamedQuery(GetSettingsQuery.class,"graphql/settings/BasicSetting.json");
The `RuleContextGenerator` is the underlying class that the `RuleExecutor` and `WorkflowExecutor` use to construct a mock `Context` for your tests. While you will most often interact with it through the executors, you can also use it directly to manually create a `Context` object.This is particularly useful when you are unit testing a helper or utility class that requires a `Context` object as a parameter, but you don't need to execute a full rule.
Key points
Core Context Builder: `RuleContextGenerator` is the fundamental builder class that the `RuleExecutor` and `WorkflowExecutor` use to create the mock `Context` for your tests.
Direct Instantiation: You can use it directly (`RuleContextGenerator.of(...)`) when you need a `Context` object for testing a helper class or utility method, but don't need to execute a full rule.
Action Tracking: It automatically captures and tracks all actions (events, mutations, logs) performed during execution, providing methods like `getActionsOfType()` and `getLastActionOfType()` for verification.
Foundation of Testing: It is the foundational component of the test utilities, providing the mock environment that makes isolated unit and integration testing possible.
Core Methods
`of` (Event, MockApiClient)
To create a `Context` manually with a specific event and mock API client, use the static factory `RuleContextGenerator.of(event, mockApiClient)`.
To replace the executor's default API mock with your own, use the `RuleContextGenerator.mockApiClient` method. This allows you to reuse the same mocks across different tests.
1// Manually create a MockApiClient for full control over API responses during the test2finalMockApiClient api =newMockApiClient();34// Inject the custom MockApiClient into the current RuleContextGenerator5RuleContextGenerator ruleContextGenerator =RuleContextGenerator.mockApiClient(api)
`getActions`
To retrieve the actions produced during execution, use the `RuleContextGenerator.getActions` method:
1// Retrieve all actions produced during rule execution for validation2List<Action> actions = ruleContextGenerator.getActions()
`getActionsOfType`
To retrieve the actions produced during execution filtered by a specific type, use the `RuleContextGenerator.getActionsOfType` method: