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