How Test Utilities work
Author:
Kirill Gaiduk
Changed on:
24 June 2025
Overview
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
- You should have knowledge of the Fluent Utility Bundles
- You should complete the Getting Started with the Fluent Utility Bundles Guide
- You should have knowledge of How Core Utilities work
- You should have knowledge of How Dynamic Utilities work
Test Utilities Overview
Author:
Kirill Gaiduk
Changed on:
14 Aug 2025
Overview
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.
Prerequisites
These 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.
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.
1package com.fluentcommerce.rule.flow;
2
3import com.fasterxml.jackson.databind.JsonNode;
4import com.fluentcommerce.util.core.JsonUtils;
5import com.fluentretail.rubix.rule.meta.EventInfo;
6import com.fluentretail.rubix.rule.meta.ParamString;
7import com.fluentretail.rubix.rule.meta.RuleInfo;
8import com.fluentretail.rubix.v2.context.Context;
9import com.fluentretail.rubix.v2.rule.Rule;
10import com.google.common.collect.ImmutableList;
11
12import static com.fluentcommerce.util.core.EventUtils.forwardInboundEventWithNewName;
13import static com.fluentcommerce.util.core.LogUtils.logOnce;
14import static com.fluentcommerce.util.dynamic.DynamicUtils.query;
15
16@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")
21public class IfPropertyIsLessThan implements Rule {
22 @Override
23 public void run(Context context) {
24 String jsonPath = context.getProp("jsonpath");
25 String value = context.getProp("value");
26
27 JsonNode result = query(context, ImmutableList.of(jsonPath)).get(jsonPath);
28
29 if(JsonUtils.marshallAndCompare(value, result) > 0) {
30 forwardInboundEventWithNewName(context, context.getProp("eventName"));
31 } else {
32 logOnce(context, IfPropertyIsLessThan.class, "Values '%s' is not less than `%s`", result, value);
33 }
34 }
35}
36
1package com.fluentcommerce.rule.flow;
2
3import com.fluentcommerce.util.test.executor.RuleContextGenerator;
4import com.fluentcommerce.util.test.executor.RuleExecutor;
5import com.fluentcommerce.util.test.executor.TestActions;
6import com.google.common.collect.ImmutableMap;
7import org.junit.jupiter.api.Test;
8
9import static org.junit.jupiter.api.Assertions.assertEquals;
10
11
12public class IfPropertyIsLessThanTest {
13
14 @Test
15 public void eventIsProducedWhenNumericValueIsLessThanValue() {
16 RuleContextGenerator context = RuleExecutor.of(IfPropertyIsLessThan.class, ImmutableMap.of(
17 "jsonpath", "fulfilmentChoice.fulfilmentPrice",
18 "value", "120",
19 "eventName", "success"
20 ))
21 .mockDynamic("graphql/order/express_one-item_all-fulfilled.json")
22 .execute();
23
24 assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(), 1);
25 assertEquals(context.getLastActionOfType(TestActions.SendEventAction.class).getEvent().getName(), "success");
26 }
27
28 @Test
29 public void eventIsNotProducedWhenNumericValueIsEqualToValue() {
30 RuleContextGenerator context = RuleExecutor.of(IfPropertyIsLessThan.class, ImmutableMap.of(
31 "jsonpath", "fulfilmentChoice.fulfilmentPrice",
32 "value", "100",
33 "eventName", "failure"
34 ))
35 .mockDynamic("graphql/order/express_one-item_all-fulfilled.json")
36 .execute();
37
38 assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(), 0);
39 }
40
41 @Test
42 public void eventIsNotProducedWhenNumericValueIsGreaterThanValue() {
43 RuleContextGenerator context = RuleExecutor.of(IfPropertyIsLessThan.class, ImmutableMap.of(
44 "jsonpath", "fulfilmentChoice.fulfilmentPrice",
45 "value", "80",
46 "eventName", "failure"
47 ))
48 .mockDynamic("graphql/order/express_one-item_all-fulfilled.json")
49 .execute();
50
51 assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(), 0);
52 }
53
54 @Test
55 public void eventIsProducedWhenStringValueIsLessThanValue() {
56 RuleContextGenerator context = RuleExecutor.of(IfPropertyIsLessThan.class, ImmutableMap.of(
57 "jsonpath", "fulfilmentChoice.deliveryType",
58 "value", "Z",
59 "eventName", "success"
60 ))
61 .mockDynamic("graphql/order/express_one-item_all-fulfilled.json")
62 .execute();
63
64 assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(), 1);
65 assertEquals(context.getLastActionOfType(TestActions.SendEventAction.class).getEvent().getName(), "success");
66 }
67
68 @Test
69 public void eventIsNotProducedWhenStringValueIsMoreThanValue() {
70 RuleContextGenerator context = RuleExecutor.of(IfPropertyIsLessThan.class, ImmutableMap.of(
71 "jsonpath", "fulfilmentChoice.deliveryType",
72 "value", "A",
73 "eventName", "failure"
74 ))
75 .mockDynamic("graphql/order/express_one-item_all-fulfilled.json")
76 .execute();
77
78 assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(), 0);
79 }
80}
81
Features
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@AllArgsConstructor
2public static class UpdateInput {
3 String id;
4 Double totalPrice;
5}
6// get variable from mutation input
7Double 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@AllArgsConstructor
2public static class UpdateInput {
3 String id;
4 Double totalPrice;
5}
6// get variable from mutation input
7UpdateInput 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:
1final Event 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.
1final Event event = Event.builder()
2 .name("Other Test")
3 .accountId("ACME")
4 .retailerId("5")
5 .rootEntityType("INVENTORY_CATALOGUE")
6 .rootEntityId("1")
7 .rootEntityRef("MASTER")
8 .entityType("INVENTORY_POSITION")
9 .entityId("1")
10 .entityRef("POS:123:456")
11 .entitySubtype("DEFAULT")
12 .entityStatus("ACTIVE")
13 .build();
14
15final Event overrides = TestUtils.eventWithDefaults(event);
Creating a RubixEntity to match the Event
To create a `RubixEntity`
with all the necessary fields for testing Orchestration and matching the event, use the `TestUtils.entityFromEvent`
method:
1Event event = TestUtils.eventWithDefaults();
2Entity primaryEntity = TestUtils.entityFromEvent(event);
Loading a Workflow from a JSON file
To load a workflow from a JSON file, use the `TestUtils.loadWorkflowFromFile`
method:
1final Workflow wf = TestUtils.loadWorkflowFromFile(filename)
Getting a Rule name from a full namespace name
To retrieve a rule name from a full namespace name, use the `TestUtils.getRuleName`
method:
1String ruleName = TestUtils.getRuleName(name)
Validating Events against @RuleInfo.produces annotation
To validate events against the `@RuleInfo.produces`
annotation, use the `TestUtils.validateEventAgainstProduces`
method:
1TestUtils.validateEventAgainstProduces(thisRule, context, rule)
Executors
Executors handle the:
- Setup
- Execution
- And validation of tests
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
Related content
Rule Executor
Author:
Kirill Gaiduk
Changed on:
28 Aug 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 query
2RuleExecutor executor = RuleExecutor.of(MyRuleToTest.class, ImmutableMap.of(
3 "eventName", "success-event"
4))
5.mockNamedQuery(
6 GetOrderQuery.class,
7 "graphql/getOrderResult.json"
8);
9
10Event 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.
1// ACT
2RuleContextGenerator context = executor.execute();
3
Assert (Verify the Outcome)
After execution, you can retrieve the actions that your rule performed and use the built-in fluent assertions to verify them.
1// ASSERT
2// Verify exactly one event was sent
3assertEquals(context.getActionsOfType(TestActions.SendEventAction.class).size(), 1);
4
5// Verify the event name
6assertEquals(
7 context.getLastActionOfType(TestActions.SendEventAction.class).getEvent().getName(),
8 "success-event"
9);
10
11// Verify no other actions were performed
12assertEquals(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.
1RuleExecutor executor = RuleExecutor.of("TestIfAttributeEquals",
2 ImmutableMap.of(
3 "event_name", "attribute-matches",
4 "attributeName", "aaa",
5 "attributeValue", "bbb"
6)).execute();
`of`
(className, props)
To create a RuleExecutor for a rule specified by its class, use the RuleExecutor.of method passing the specific rule class.
1RuleContextGenerator context = RuleExecutor.of(
2 TestUpdateOrder.class,
3 ImmutableMap.of()
4)
5.execute();
`execute`
(event)
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 Order entity.
1RuleContextGenerator context = RuleExecutor.of(IfPropertyEquals.class,
2 ImmutableMap.of(
3 "jsonpath", "event.attributes.myEventAttribute",
4 "value", "YES",
5 "eventName", "success"
6 )
7)
8.execute(Event.builder().attributes(ImmutableMap.of("myEventAttribute", "YES")).build());
`resetApi`
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.
1final MockApiClient api = new MockApiClient();
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.
1@Mock
2private ReadOnlyFluentApiClient apiClient;
3.....
4RuleExecutor RuleExecutor = executor.withLegacyClient(apiClient)
Complete Example
1import com.fluentcommerce.util.test.executor.RuleExecutor;
2import com.fluentcommerce.util.test.executor.RuleContextGenerator;
3import com.fluentretail.rubix.event.Event;
4import org.junit.jupiter.api.Test;
5
6import static com.fluentcommerce.util.test.executor.TestActions.SendEventAction;
7import static org.junit.jupiter.api.Assertions.*;
8
9public class MyRuleTest {
10
11 @Test
12 void testMyRule() {
13 // ARRANGE: Set up the rule executor with properties
14 RuleExecutor executor = RuleExecutor.of(MyRule.class, ImmutableMap.of(
15 "eventName", "success-event",
16 "threshold", "100"
17 ))
18 .mockNamedQuery(
19 GetOrderQuery.class,
20 "graphql/getOrderResult.json"
21 );
22
23 // Create custom event
24 Event customEvent = Event.builder()
25 .name("OrderCreated")
26 .entityRef("order-123")
27 .entityType("ORDER")
28 .attributes(ImmutableMap.of(
29 "orderValue", "150.00",
30 "customerType", "PREMIUM"
31 ))
32 .build();
33
34 // ACT: Execute the rule with custom event
35 RuleContextGenerator context = executor.execute(customEvent);
36
37 // ASSERT: Verify the results
38 assertEquals(1, context.getActionsOfType(SendEventAction.class).size());
39 assertEquals("success-event", context.getLastActionOfType(SendEventAction.class).getEvent().getName());
40
41 // Verify no other actions were performed
42 assertEquals(0, context.getActionsOfType(MutateAction.class).size());
43 assertEquals(0, context.getActionsOfType(WebhookAction.class).size());
44 }
45}
Related content
Workflow Executor
Author:
Kirill Gaiduk
Changed on:
28 Aug 2025
Overview
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.
1WorkflowExecutor workflowExecutor = WorkflowExecutor.of("workflows/basic-order.json");
`mockRule`
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(
2 Event.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.
1workflowExecutor.mockNamedQuery("ValidateOrder",
2 GetOrderWithItemsAndFulfilmentsQuery.class,
3 "graphql/GetOrderWithItemsAndFulfilments/one-item_none-fulfilled.json")
After running the `WorkflowExecutor`
, it returns a `RuleContextGenerator`
that you can use to assert the expected outcomes of the Workflow execution.
1RuleContextGenerator context = WorkflowExecutor.of("workflows/basic-order.json")
2 .mockRule(
3 "SendEvent", c -> c.context().action().sendEvent(
4 Event.builder().name((String) c.getRule().getProps().get("eventName")).build()))
5 .mockNamedQuery("ValidateOrder",
6 GetOrderWithItemsAndFulfilmentsQuery.class,
7 "graphql/GetOrderResponse.json")
8 .execute(
9 Event.builder().name("CREATE").build());
10assertEquals(2, context.getActionsOfType(TestActions.SendEventAction.class).size());
11assertEquals("ValidateOrder", context.getLastActionOfType(TestActions.SendEventAction.class).getEvent().getName());
`ignoreRules`
In order to ignore a specific set of rules during test execution, use the `WorkflowExecutor.ignoreRules`
method.
1WorkflowExecutor.ignoreRules(
2 "TEST.mock.LogEvent",
3 "TEST.mock.SendWebhook"
4)
`execute`
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 order entity.
1TestContext testContext = WorkflowExecutor.execute()
`execute`
(event)
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.
1Event createEvent = Event.builder()
2 .name("CREATE")
3 .entityRef("order-123")
4 .build();
5TestContext testContext = RuleContextGenerator context = executor.execute(createEvent);
Complete Example
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.
1{
2 "name": "CREATE",
3 "rules": [
4 {
5 "name": "TEST.custom.ValidateOrder",
6 "props": null
7 },
8 {
9 "name": "TEST.custom.SendOrderWebhookEvent",
10 "props": {
11 "eventName": "SendOrderCreateWebhook"
12 }
13 },
14 {
15 "name": "TEST.core.SendEvent",
16 "props": {
17 "eventName": "RunSourcingLogic"
18 }
19 }
20 ]
21}
2. Test Class
The test class uses the `WorkflowExecutor`
to load the workflow, mock any necessary dependencies, and execute it with an initial event.
1import com.fluentcommerce.util.test.executor.WorkflowExecutor;
2import com.fluentcommerce.util.test.executor.RuleContextGenerator;
3import com.fluentretail.rubix.event.Event;
4import org.junit.jupiter.api.Test;
5
6public class MyOrderWorkflowTest {
7
8 @Test
9 void test_FullOrderWorkflow() {
10 // 1. Arrange: Load the workflow file
11 WorkflowExecutor executor = WorkflowExecutor.of("workflows/my_order_workflow.json")
12 // Mock a GraphQL query for a specific rule in the workflow
13 .mockNamedQuery(
14 "TEST.custom.ValidateOrder",
15 GetOrderByIdQuery.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 );
24
25 // 2. Act: Execute the workflow with an initial event
26 Event initialEvent = Event.builder()
27 .name("CREATE")
28 .entityRef("order-123")
29 .build();
30 RuleContextGenerator context = executor.execute(initialEvent);
31
32 // 3. Assert: Verify the final state
33 // Check the sequence of events that were created
34 assertEquals(1, context.getActionsOfType(SendEventAction.class).size());
35 assertEquals("RunSourcingLogic", context.getLastActionOfType(SendEventAction.class).getEvent().getName());
36
37 // Check that the mocked rule's log message exists
38 assertEquals(1, context.getActionsOfType(LogAction.class).size());
39 assertTrue(context.getLastActionOfType(LogAction.class).getMessage().contains("SendOrderWebhookEvent rule was called"));
40 }
41}
Related content
MockApiClient
Author:
Kirill Gaiduk
Changed on:
24 June 2025
Overview
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 = new MockApiClient();
2// now mock any responses to `api.query(GetSettingsQuery.builder().build())`
3api.mockNamedQuery(GetSettingsQuery.class, "graphql/settings/BasicSetting.json");
Related content
Rule Context Generator
Author:
Kirill Gaiduk
Changed on:
28 Aug 2025
Overview
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)`
.
1MockApiClient mockApi = new MockApiClient();
2Event testEvent = Event.builder()
3 .name("OrderCreated")
4 .entityRef("order-123")
5 .build();
6
7RuleContextGenerator contextGenerator = RuleContextGenerator.of(testEvent, mockApi.get());
`of`
(MockApiClient)
To create a `Context`
manually with a specific mock API client, use the static factory `RuleContextGenerator.of( mockApiClient)`
.
1MockApiClient mockApi = new MockApiClient();
2RuleContextGenerator contextGenerator = RuleContextGenerator.of(mockApi.get());
`forRule`
To configure the context for a specific rule instance, use the `forRule()`
method. This sets up the rule properties and configuration.
1RuleInstance ruleInstance = RuleInstance.builder()
2 .name("sampleRule")
3 .props(ImmutableMap.of("eventName", "my-custom-event"))
4 .build();
5
6RuleContextGenerator.RuleTestContext ruleContext = contextGenerator.forRule(ruleInstance);
`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 test
2final MockApiClient api = new MockApiClient();
3
4// Inject the custom MockApiClient into the current RuleContextGenerator
5RuleContextGenerator 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 validation
2List<Action> actions = ruleContextGenerator.getActions()
`getActionsOfType`
To retrieve the actions produced during execution filtered by a specific type, use the `RuleContextGenerator.getActionsOfType`
method:
1RuleContextGenerator context = RuleExecutor.of(
2 TestSendEvent.class,
3 ImmutableMap.of(EVENT_NAME, "success")
4 )
5 .execute();
6List<SendEventAction> sendEventActions = context.getActionsOfType(SendEventAction.class);
7assertEquals(sendEventActions.size(), 1);
`getFirstActionOfType`
To retrieve the first action of a given type that was produced during execution, use the `RuleContextGenerator.getFirstActionOfType`
method:
1RuleContextGenerator context = RuleExecutor.of(
2 TestSendWebhook.class,
3 ImmutableMap.of("endpoint", "http://fluentcommerce.com")
4 )
5 .execute();
6WebhookAction firstWebhookAction = context.getFirstActionOfType(WebhookAction.class);
7assertEquals(firstWebhookAction.getEvent().getName(), "my-webhook");
`getLastActionOfType`
To retrieve the last action of a given type that was produced during execution, use the `RuleContextGenerator.getLastActionOfType`
method:
1RuleContextGenerator context = RuleExecutor.of(
2 TestSendWebhook.class,
3 ImmutableMap.of("endpoint", "http://fluentcommerce.com")
4 )
5 .execute();
6WebhookAction lastWebhookAction = context.getLastActionOfType(WebhookAction.class);
7assertEquals(lastWebhookAction.getWebhookEndpoint(), "http://fluentcommerce.com");
Complete Example
Here's a complete example showing how to use `RuleContextGenerator`
directly:
1import com.fluentcommerce.util.test.executor.RuleContextGenerator;
2import com.fluentcommerce.util.test.executor.MockApiClient;
3import com.fluentretail.rubix.context.Context;
4import com.fluentretail.rubix.event.Event;
5import com.fluentretail.rubix.rule.RuleInstance;
6import org.junit.jupiter.api.Test;
7
8import static com.fluentcommerce.util.test.executor.TestActions.SendEventAction;
9import static org.junit.jupiter.api.Assertions.*;
10
11public class MyUtilityTest {
12
13 @Test
14 void testMyUtilityMethod() {
15 // 1. CREATE: Set up the mock API client
16 MockApiClient mockApi = new MockApiClient();
17
18 // 2. CREATE: Create the event
19 Event testEvent = Event.builder()
20 .name("OrderCreated")
21 .entityRef("order-123")
22 .entityType("ORDER")
23 .attributes(ImmutableMap.of(
24 "orderValue", "150.00",
25 "customerType", "PREMIUM"
26 ))
27 .build();
28
29 // 3. CREATE: Create the RuleContextGenerator
30 RuleContextGenerator contextGenerator = RuleContextGenerator.of(testEvent, mockApi.get());
31
32 // 4. CONFIGURE: Set up the rule instance
33 RuleInstance ruleInstance = RuleInstance.builder()
34 .name("MyTestRule")
35 .props(ImmutableMap.of(
36 "eventName", "success-event",
37 "threshold", "100"
38 ))
39 .build();
40
41 // 5. CONFIGURE: Configure the context for the rule
42 RuleContextGenerator.RuleTestContext ruleContext = contextGenerator.forRule(ruleInstance);
43
44 // 6. BUILD: Create the final Context object
45 Context context = contextGenerator.build();
46
47 // 7. EXECUTE: Run your utility method or rule
48 MyUtilityClass utility = new MyUtilityClass();
49 utility.processOrder(context);
50
51 // 8. VERIFY: Check the results
52 assertEquals(1, contextGenerator.getActionsOfType(SendEventAction.class).size());
53 assertEquals("success-event", contextGenerator.getLastActionOfType(SendEventAction.class).getEvent().getName());
54 }
55}