Fluent Commerce Logo
Docs
Sign In

Test Utilities Overview

Essential knowledge

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