Hooks

Advanced Example

ReṼoman isn’t limited to firing a collection — you can add more bells and whistles 🔔. This end-to-end example wires hooks, type-safe request/response config, dynamic variables, and execution control together:

Live include from a passing integration test → PQE2EWithSMTest.java
final var pqRundown =
    ReVoman.revUp( (1)
        Kick.configure()
            .templatePaths(PQ_TEMPLATE_PATHS) (2)
            .environmentPath(PQ_ENV_PATH) (3)
            .dynamicEnvironment( (4)
                Map.of(
                    "$quoteFieldsToQuery", "LineItemCount, CalculationStatus",
                    "$qliFieldsToQuery", "Id, Product2Id",
                    "$qlrFieldsToQuery", "Id, QuoteId, MainQuoteLineId, AssociatedQuoteLineId"))
            .customDynamicVariableGenerator( (5)
                "$unitPrice",
                (ignore1, ignore2, ignore3) -> String.valueOf(Random.Default.nextInt(999) + 1))
            .nodeModulesPath("js") (6)
            .haltOnFailureOfTypeExcept(
                HTTP_STATUS, afterStepContainingHeader("ignoreHTTPStatusUnsuccessful")) (7)
            .requestConfig( (8)
                unmarshallRequest(
                    beforeStepContainingURIPathOfAny(PQ_URI_PATH),
                    PlaceQuoteInputRepresentation.class,
                    adapter(PlaceQuoteInputRepresentation.class)))
            .responseConfig( (9)
                unmarshallResponse(
                    afterStepContainingURIPathOfAny(PQ_URI_PATH),
                    PlaceQuoteOutputRepresentation.class),
                unmarshallResponse(
                    afterStepContainingURIPathOfAny(COMPOSITE_GRAPH_URI_PATH),
                    CompositeGraphResponse.class,
                    CompositeGraphResponse.ADAPTER))
            .hooks( (10)
                pre(
                    beforeStepContainingURIPathOfAny(PQ_URI_PATH),
                    (step, requestInfo, rundown) -> {
                      if (requestInfo.containsHeader(IS_SYNC_HEADER)) {
                        LOGGER.info("This is a Sync step: {}", step);
                      }
                    }),
                post(
                    afterStepContainingURIPathOfAny(PQ_URI_PATH),
                    (stepReport, ignore) -> validatePQResponse(stepReport)), (11)
                post(
                    afterStepContainingURIPathOfAny(COMPOSITE_GRAPH_URI_PATH),
                    (stepReport, ignore) -> assertCompositeGraphResponseSuccess(stepReport)),
                post(
                    afterStepName("query-quote-and-related-records"),
                    (ignore, rundown) -> assertAfterPQCreate(rundown.mutableEnv)))
            .pollingConfig( (12)
                poll((stepReport, rundown) ->
                        uriPathContains(stepReport.requestInfo, PQ_URI_PATH)
                            && !containsHeader(stepReport.requestInfo, IS_SYNC_HEADER))
                    .request(
                        (stepReport, env) ->
                            Request.create(
                                Method.GET,
                                "%s/%s/sobjects/Quote/%s"
                                    .formatted(
                                        env.getAsString("baseUrl"),
                                        env.getAsString("versionPath"),
                                        env.getAsString("quoteId"))))
                    .every(Duration.ofSeconds(2))
                    .timeout(Duration.ofSeconds(30))
                    .until((response, env) -> response.bodyString().contains("Completed")))
            .globalCustomTypeAdapter(IDAdapter.INSTANCE) (13)
            .insecureHttp(true) (14)
            .off()); // Kick-off
assertThat(pqRundown.firstUnIgnoredUnsuccessfulStepReport()).isNull(); (15)
assertThat(pqRundown.mutableEnv)
    .containsAtLeastEntriesIn(
        Map.of(
            "quoteCalculationStatusForSkipPricing", PricingPref.Skip.completeStatus,
            "quoteCalculationStatus", PricingPref.System.completeStatus,
            "quoteCalculationStatusAfterAllUpdates", PricingPref.System.completeStatus));
1 revUp() is the method to call passing a configuration, built as below
2 Supply the path (absolute or relative to resources) to the Template Collection JSON file/files
3 Supply the path (absolute or relative to resources) to the Environment JSON file/files
4 Supply any dynamic environment that is runtime-specific
5 Custom Dynamic variables
6 Node modules path (absolute or relative to project directory) to be used inside Pre-req and Post-res scripts
7 Execution Control
8 Request Config
9 Response Config
10 Dynamic Pre/Post-Step Hooks
11 Response Validations
12 Polling config for async steps
13 Global Custom Type Adapters
14 Ignore Java cert issues when firing HTTP calls. To be used only for local testing, like hitting localhost
15 Run more assertions on the Rundown

Dynamic Pre-Step and Post-Step Hooks

A hook lets you fiddle with the execution by plugging in your custom JVM code before or after a Step execution.

You can pass a PreTxnStepPick/PostTxnStepPick which is a Predicate used to qualify a step for Pre-Step/Post-Step Hook respectively. The Predicate enables Dynamic hooks, which can fire for multiple qualified steps.

ReṼoman comes bundled with some predicates under PreTxnStepPick.PickUtils/PostTxnStepPick.PickUtils e.g. beforeStepContainingURIPathOfAny(), afterStepName() etc. If those don’t fit, write your own:

final var preTxnStepPick = (currentStep, requestInfo, rundown) -> LOGGER.info("Picked `preLogHook` before stepName: {}", currentStep)
final var postTxnStepPick = (stepReport, rundown) -> LOGGER.info("Picked `postLogHook` after stepName: {}", stepReport.step.displayName)

Add them to the config:

.hooks(
    pre(
            preTxnStepPick,
            (currentStepName, requestInfo, rundown) -> {
                //...code...
            }),
    post(
            postTxnStepPick,
            (currentStepName, rundown) -> {
                //...code...
            })
)
  • You can assert on the rundown, do Response Validations, or even mutate the environment with a programmatically derived value, so later steps pick up those changes.

  • Reserve hooks for plugging in custom code or asserting to fail-fast mid-execution. If your assertions can wait till the final rundown, it’s cleaner to write them after revUp() returns the rundown.

Plug-in your Custom JVM code in-between executions

You can plug in custom JVM code to create/generate values for environment variables, populated and picked up by subsequent steps. For example, you may want some xyzId but you don’t have a collection step to create it — you have a Java utility instead. Invoke it in a pre-hook and set the value in rundown.mutableEnv, so later steps pick up {{xyzId}} from the environment.

When a Step qualifies for more than one hook, hooks execute in the same sequence they are configured (via hooks() or overrideHooks()). Both accept an iterable — pass an Ordered Iterable (like ArrayList) if order matters.

Response Validations

  • Post-Hooks are the best place to validate response right after the step.

  • If you configured a strong type via responseConfig, write type-safe validations by extracting your type with stepReport.responseInfo.get().<TypeT>getTypedTxnObj() (if responseConfig()/globalCustomTypeAdapters() configured) or JsonPojoUtils.jsonToPojo(TypeT, stepReport.responseInfo.get().httpMsg.bodyString()) inline.

  • If your response is non-trivial and needs strategies like fail-fast or error-accumulation, consider a library like Vador (github.com/salesforce-misc/Vador).