Liking cljdoc? Tell your friends :D

Replay Testing

Why replay testing?

Temporal workflows must be deterministic: replaying a workflow's persisted Event History against its code must produce the identical command sequence. Any change that reorders activities, adds or removes steps without versioning, branches on non-deterministic inputs (wall-clock time, random values, external state), or upgrades a library that changes internal behavior will break replay — silently, until a worker tries to resume an in-flight workflow in production.

The standard mitigation is the capture-then-replay pattern:

  1. Capture representative production (or test-environment) histories as JSON fixtures.
  2. In CI, replay those fixtures against the current code.
  3. Fail the build on any mismatch.

The Temporal Java SDK ships WorkflowReplayer and WorkflowExecutionHistory for exactly this purpose. This SDK exposes them as idiomatic Clojure through two namespaces: temporal.testing.history and temporal.testing.replayer.

For background, see the Temporal Testing Suite and Event History Walkthrough in the upstream docs.


The capture-then-replay workflow

┌─────────────────────────────────────────────────────────────┐
│  1. Run workflow in test env (or production)                 │
│  2. Capture history with history/fetch or history/->json     │
│  3. Save JSON fixture to test/resources/histories/           │
│  4. In CI: replay fixture with replayer/replay-history       │
│             or replayer/replay-all for batches               │
│  5. Fail build if replay throws                              │
└─────────────────────────────────────────────────────────────┘

The two namespaces map cleanly onto these phases:

NamespaceRole
temporal.testing.historyLoad, capture, and serialize event histories
temporal.testing.replayerReplay histories against current workflow code

Loading and capturing histories

Require both namespaces:

(require '[temporal.testing.history :as history]
         '[temporal.testing.replayer :as replayer])

From a classpath resource (most common in CI)

Store your history fixtures under test/resources/histories/ so they land on the test classpath:

(history/from-resource "histories/order-workflow.json")

Throws a clear ex-info with {:resource name} if the file is absent.

From a file path

(history/from-file "test/resources/histories/order-workflow.json")
;; also accepts java.io.File and java.nio.file.Path

From a raw JSON string

(history/from-json (slurp "order.json"))
;; optional 2-arity form overrides the embedded workflow ID
(history/from-json json "my-workflow-id")

Capturing a history from a live client

After running a workflow in a temporal.testing.env or against a real server, fetch the history and save it:

(let [h (history/fetch client "workflow-id-123")]
  (spit "test/resources/histories/order.json" (history/->json h)))

->json defaults to pretty-printed output. Pass false for compact:

(history/->json h false)

Inspecting histories at the REPL

Every WorkflowExecutionHistory implements clojure.core.protocols/Datafiable:

(require '[clojure.datafy :as d])

(d/datafy (history/from-resource "histories/order-workflow.json"))
;; => {:workflow-id "order-123"
;;     :run-id      "abc-..."
;;     :events      [#object[...] ...]}

Full API: temporal.testing.history


Replaying a history

One-shot replay

The simplest form — no setup required:

(replayer/replay-history (history/from-resource "histories/order-workflow.json"))

With a custom data converter:

(replayer/replay-history h {:data-converter my-converter})

Throws ex-info with {:workflow-id "..." :cause #<Exception>} on any non-determinism error.

Reusable replayer handle

When replaying multiple histories, reuse a single replayer to avoid spinning up a TestWorkflowEnvironment per call:

(with-open [r (replayer/create)]
  (replayer/replay r (history/from-resource "histories/order-workflow.json"))
  (replayer/replay r (history/from-resource "histories/payment-workflow.json")))

create accepts an options map:

KeyDescriptionTypeDefault
:dispatchExplicit workflow dispatch tablemapauto-dispatch
:data-converterCustom data converterDataConverterdefault

The returned value implements java.io.Closeable, so with-open handles shutdown automatically.

Full API: temporal.testing.replayer


Batch replay in CI

For CI pipelines, load all your history fixtures and replay them in one call:

(ns my.replay-test
  (:require [clojure.test :refer :all]
            [clojure.java.io :as io]
            [temporal.testing.history :as history]
            [temporal.testing.replayer :as replayer]))

(deftest replay-all-fixtures
  (let [dir      (io/file "test/resources/histories")
        histories (map history/from-file (.listFiles dir))]
    (with-open [r (replayer/create)]
      (let [{:keys [total failures]} (replayer/replay-all r histories {:fail-fast? false})]
        (is (zero? (count failures))
            (str "Non-determinism in " (count failures) "/" total " workflows: "
                 (map :workflow-id failures)))))))

replay-all returns a plain Clojure map:

{:total    5
 :failures [{:workflow-id "order-123" :error #<NonDeterminismException>}]}

The :fail-fast? option (default false) controls whether replay stops at the first failure or collects all failures.

For a complete runnable example, see the replay-testing sample.


Connection to versioning

Replay testing and workflow versioning work together. Whenever you make a code change that would alter the command sequence of an in-flight workflow, you must guard it with temporal.workflow/get-version. A replay test will immediately catch you if you forget:

(require '[temporal.workflow :as w]
         '[temporal.activity :as a])

;; Safely change from invoke → local-invoke without breaking existing histories
(let [version (w/get-version ::local-activity w/default-version 1)]
  (cond
    (= version w/default-version) @(a/invoke my-activity args)
    (= version 1)                 @(a/local-invoke my-activity args)))

See the Versioning section in the Workflows guide for the full pattern and the upstream Versioning docs.

Workflow code changes that require versioning:

  • Adding or removing activity invocations
  • Reordering commands
  • Adding timers or side effects
  • Changing workflow arguments used for branching

Changes that do NOT require versioning:

  • Activity implementation changes (only the command, not its result, is replayed)
  • Bug fixes that don't alter the command sequence
  • Adding new signals or queries not exercised by existing histories

API reference

temporal.testing.history

FunctionDescription
from-jsonParse a JSON string into a WorkflowExecutionHistory
from-fileRead a history from a file path, File, or Path
from-resourceRead a history from a classpath resource
fetchFetch a history from a live WorkflowClient
->jsonSerialize a history to a JSON string

temporal.testing.replayer

FunctionDescription
createCreate a reusable replayer handle (Closeable)
replayReplay a single history; throws on non-determinism
replay-allBatch replay; returns {:total n :failures [...]}
replay-historyOne-shot convenience: create → replay → close
closeAsync shutdown
synchronized-stopBlocking shutdown

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close