Cadence Workflow Testing Patterns: From Unit to Integration
Cadence is Uber's open-source workflow engine—the direct predecessor to Temporal. While many teams have migrated to Temporal, Cadence remains widely deployed at scale, and understanding its testing patterns is valuable both for maintaining Cadence codebases and for teams evaluating the migration path to Temporal. This guide covers Cadence workflow testing in Java using TestWorkflowRule, including time skipping, activity mocking, determinism testing, and migration considerations.
Cadence vs Temporal: Testing Differences
Before diving in, it helps to understand how Cadence and Temporal differ in their testing infrastructure:
| Feature | Cadence | Temporal |
|---|---|---|
| Test rule class | TestWorkflowRule |
TestWorkflowExtension / TestWorkflowEnvironment |
| Mock framework | Mockito | Mockito |
| Time skipping | testWorkflowRule.getTestEnvironment().sleep(duration) |
testEnv.sleep(duration) |
| Workflow interface | @WorkflowMethod |
@WorkflowMethod |
| Activity interface | @ActivityMethod |
@ActivityMethod |
| Java package | com.uber.cadence |
io.temporal |
| SDK | com.uber.cadence:cadence-client |
io.temporal:temporal-sdk |
The concepts are nearly identical, but package names and some class names differ.
Setting Up Cadence Testing in Java
Add the Cadence client and test dependencies to your pom.xml:
<dependencies>
<dependency>
<groupId>com.uber.cadence</groupId>
<artifactId>cadence-client</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.0</version>
<scope>test</scope>
</dependency>
</dependencies>For Gradle:
dependencies {
implementation 'com.uber.cadence:cadence-client:2.7.9'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.8.0'
}TestWorkflowRule Basics
TestWorkflowRule is a JUnit 4 rule that creates an in-process Cadence test server. It provides all the infrastructure needed to run workflow tests without a real Cadence cluster:
import com.uber.cadence.testing.TestWorkflowRule;
import com.uber.cadence.testing.TestWorkflowEnvironment;
import com.uber.cadence.client.WorkflowClient;
import com.uber.cadence.worker.Worker;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
import java.time.Duration;
public class OrderWorkflowTest {
@Rule
public TestWorkflowRule testWorkflowRule = TestWorkflowRule.newBuilder()
.setWorkflowTypes(OrderWorkflowImpl.class)
.setDoNotStart(true) // We'll configure mocks before starting
.build();
@Test
public void testOrderWorkflowSuccess() {
// Create mock activities
OrderActivities orderActivities = Mockito.mock(OrderActivities.class);
Mockito.when(orderActivities.validateOrder(Mockito.anyString()))
.thenReturn(true);
Mockito.when(orderActivities.reserveInventory(Mockito.anyString()))
.thenReturn("reservation-456");
Mockito.when(orderActivities.chargeCustomer(Mockito.anyString()))
.thenReturn("charge-789");
Mockito.when(orderActivities.fulfillOrder(Mockito.anyString()))
.thenReturn("tracking-ABC123");
// Register mock activities with the worker
testWorkflowRule.getWorker().registerActivitiesImplementations(orderActivities);
testWorkflowRule.getTestEnvironment().start();
// Get a workflow stub
OrderWorkflow workflow = testWorkflowRule.getWorkflowClient().newWorkflowStub(
OrderWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setExecutionStartToCloseTimeout(Duration.ofSeconds(30))
.build()
);
// Execute and assert
String result = workflow.processOrder("order-123");
assertEquals("Order order-123 fulfilled: tracking-ABC123", result);
// Verify all activities were called
Mockito.verify(orderActivities).validateOrder("order-123");
Mockito.verify(orderActivities).reserveInventory("order-123");
Mockito.verify(orderActivities).chargeCustomer("order-123");
Mockito.verify(orderActivities).fulfillOrder("order-123");
}
}The setDoNotStart(true) pattern is important—it lets you register activity implementations (mocks) before the worker starts, preventing races where the worker might try to process tasks before mocks are configured.
Defining Workflow and Activity Interfaces
Here's a complete example showing the workflow and activity interfaces along with their implementations:
// Workflow interface
@WorkflowInterface
public interface OrderWorkflow {
@WorkflowMethod
String processOrder(String orderId);
@SignalMethod
void cancelOrder(String reason);
@QueryMethod
String getStatus();
}
// Activity interface
@ActivityInterface
public interface OrderActivities {
@ActivityMethod(scheduleToCloseTimeoutSeconds = 10)
boolean validateOrder(String orderId);
@ActivityMethod(scheduleToCloseTimeoutSeconds = 30)
String reserveInventory(String orderId);
@ActivityMethod(scheduleToCloseTimeoutSeconds = 60)
String chargeCustomer(String orderId);
@ActivityMethod(scheduleToCloseTimeoutSeconds = 120)
String fulfillOrder(String orderId);
}
// Workflow implementation
public class OrderWorkflowImpl implements OrderWorkflow {
private final OrderActivities activities = Workflow.newActivityStub(
OrderActivities.class,
new ActivityOptions.Builder()
.setScheduleToCloseTimeout(Duration.ofSeconds(120))
.setRetryOptions(new RetryOptions.Builder()
.setMaximumAttempts(3)
.setInitialInterval(Duration.ofSeconds(1))
.build())
.build()
);
private String status = "PENDING";
private boolean cancelled = false;
private String cancelReason = null;
@Override
public String processOrder(String orderId) {
status = "VALIDATING";
if (!activities.validateOrder(orderId)) {
status = "INVALID";
return "Order " + orderId + " is invalid";
}
status = "RESERVING_INVENTORY";
String reservation = activities.reserveInventory(orderId);
// Check cancellation after each major step
if (cancelled) {
status = "CANCELLED";
return "Order " + orderId + " cancelled: " + cancelReason;
}
status = "CHARGING";
String chargeId = activities.chargeCustomer(orderId);
status = "FULFILLING";
String trackingNumber = activities.fulfillOrder(orderId);
status = "COMPLETED";
return "Order " + orderId + " fulfilled: " + trackingNumber;
}
@Override
public void cancelOrder(String reason) {
cancelled = true;
cancelReason = reason;
}
@Override
public String getStatus() {
return status;
}
}Testing with Time Skipping
Cadence's test environment supports time skipping—advancing the simulated clock without waiting real time. This is essential for testing workflows that use timers, deadlines, or Workflow.sleep():
@WorkflowInterface
public interface SubscriptionWorkflow {
@WorkflowMethod
void processSubscription(String customerId);
}
public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
private final BillingActivities billing = Workflow.newActivityStub(BillingActivities.class);
@Override
public void processSubscription(String customerId) {
// Bill monthly for 12 months
for (int month = 1; month <= 12; month++) {
billing.chargeMonthlyFee(customerId, month);
if (month < 12) {
Workflow.sleep(Duration.ofDays(30)); // Sleep 30 days between charges
}
}
}
}
public class SubscriptionWorkflowTest {
@Rule
public TestWorkflowRule testWorkflowRule = TestWorkflowRule.newBuilder()
.setWorkflowTypes(SubscriptionWorkflowImpl.class)
.setDoNotStart(true)
.build();
@Test
public void testSubscriptionChargesAllMonths() throws InterruptedException {
BillingActivities billing = Mockito.mock(BillingActivities.class);
testWorkflowRule.getWorker().registerActivitiesImplementations(billing);
testWorkflowRule.getTestEnvironment().start();
WorkflowClient client = testWorkflowRule.getWorkflowClient();
SubscriptionWorkflow workflow = client.newWorkflowStub(
SubscriptionWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setExecutionStartToCloseTimeout(Duration.ofDays(400))
.build()
);
// Start workflow asynchronously
WorkflowClient.start(workflow::processSubscription, "customer-456");
// Skip through 11 monthly sleep periods
TestWorkflowEnvironment testEnv = testWorkflowRule.getTestEnvironment();
for (int i = 0; i < 11; i++) {
testEnv.sleep(Duration.ofDays(30));
}
// Wait for completion
WorkflowStub workflowStub = client.newUntypedWorkflowStub("SubscriptionWorkflow", Optional.empty(), Optional.empty());
workflowStub.getResult(Void.class);
// Verify all 12 charges
for (int month = 1; month <= 12; month++) {
Mockito.verify(billing).chargeMonthlyFee("customer-456", month);
}
}
@Test
public void testSubscriptionAfterFiveMonths() throws InterruptedException {
BillingActivities billing = Mockito.mock(BillingActivities.class);
testWorkflowRule.getWorker().registerActivitiesImplementations(billing);
testWorkflowRule.getTestEnvironment().start();
WorkflowClient client = testWorkflowRule.getWorkflowClient();
SubscriptionWorkflow workflow = client.newWorkflowStub(
SubscriptionWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setExecutionStartToCloseTimeout(Duration.ofDays(400))
.build()
);
WorkflowClient.start(workflow::processSubscription, "customer-789");
// Only skip 4 sleep periods (advance through 5 months)
TestWorkflowEnvironment testEnv = testWorkflowRule.getTestEnvironment();
for (int i = 0; i < 4; i++) {
testEnv.sleep(Duration.ofDays(30));
}
// After 5 months, only 5 charges should have happened
Mockito.verify(billing, Mockito.times(5)).chargeMonthlyFee(
Mockito.eq("customer-789"), Mockito.anyInt()
);
Mockito.verify(billing, Mockito.never()).chargeMonthlyFee("customer-789", 6);
}
}Testing Workflow Determinism
Determinism is a core constraint of workflow code. Non-deterministic code causes "non-determinism error" when the workflow is replayed from history. Common mistakes include:
- Using
Math.random()orUUID.randomUUID()directly - Calling
System.currentTimeMillis()ornew Date()directly - Accessing mutable static state
- Using non-deterministic data structures (e.g., iteration order of
HashMap)
Here's how to write a determinism test:
public class DeterminismTest {
@Rule
public TestWorkflowRule testWorkflowRule = TestWorkflowRule.newBuilder()
.setWorkflowTypes(DataProcessingWorkflowImpl.class)
.setDoNotStart(true)
.build();
@Test
public void testWorkflowIsDeterministic() {
DataActivities activities = Mockito.mock(DataActivities.class);
Mockito.when(activities.fetchData(Mockito.anyString()))
.thenReturn(Arrays.asList("item-1", "item-2", "item-3"));
Mockito.when(activities.processItem(Mockito.anyString()))
.thenReturn("processed");
testWorkflowRule.getWorker().registerActivitiesImplementations(activities);
testWorkflowRule.getTestEnvironment().start();
WorkflowClient client = testWorkflowRule.getWorkflowClient();
DataProcessingWorkflow workflow = client.newWorkflowStub(
DataProcessingWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
.build()
);
// Run workflow twice with same input, expect same output
String result1 = workflow.processDataSet("dataset-A");
// Reset and run again
testWorkflowRule.getTestEnvironment().close();
// Recreate test environment and run again
// (In practice, you'd use WorkflowReplayer for proper determinism testing)
// The key assertion: same input always produces same output
assertEquals("Expected deterministic output for dataset-A", "dataset-A-3-items", result1);
}
}For true determinism testing, use the WorkflowReplayer:
import com.uber.cadence.testing.WorkflowReplayer;
import com.uber.cadence.internal.testing.WorkflowTestingOptions;
public class ReplayDeterminismTest {
@Test
public void testWorkflowReplay() throws Exception {
// Load a previously captured workflow history
// (Captured from production or a previous test run)
WorkflowReplayer.replayWorkflowExecutionFromResource(
"test-history/order-workflow-history.json",
OrderWorkflowImpl.class
);
// No exception = workflow is deterministic
}
}To capture workflow history for replay tests:
# Using the Cadence CLI to get workflow history
cadence --domain my-domain workflow showid --wid my-workflow-id --rid my-run-idTesting Signals in Cadence Workflows
public class SignalWorkflowTest {
@Rule
public TestWorkflowRule testWorkflowRule = TestWorkflowRule.newBuilder()
.setWorkflowTypes(ApprovalWorkflowImpl.class)
.setDoNotStart(true)
.build();
@Test
public void testApprovalSignalApproved() {
ApprovalActivities activities = Mockito.mock(ApprovalActivities.class);
Mockito.when(activities.submitForReview(Mockito.anyString()))
.thenReturn("review-submitted");
Mockito.when(activities.executeApproved(Mockito.anyString()))
.thenReturn("approved-and-done");
testWorkflowRule.getWorker().registerActivitiesImplementations(activities);
testWorkflowRule.getTestEnvironment().start();
WorkflowClient client = testWorkflowRule.getWorkflowClient();
// Create workflow with specific ID so we can signal it later
ApprovalWorkflow workflow = client.newWorkflowStub(
ApprovalWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setWorkflowId("approval-test-001")
.setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
.build()
);
// Start asynchronously
WorkflowClient.start(workflow::processRequest, "request-001");
// Send signal
ApprovalWorkflow workflowById = client.newWorkflowStub(
ApprovalWorkflow.class, "approval-test-001"
);
workflowById.approve("manager-approved");
// Wait for result
String result = WorkflowStub.fromTyped(workflow).getResult(String.class);
assertEquals("approved-and-done", result);
Mockito.verify(activities).executeApproved("request-001");
}
@Test
public void testApprovalSignalRejected() {
ApprovalActivities activities = Mockito.mock(ApprovalActivities.class);
Mockito.when(activities.submitForReview(Mockito.anyString()))
.thenReturn("review-submitted");
Mockito.when(activities.executeRejected(Mockito.anyString(), Mockito.anyString()))
.thenReturn("rejected");
testWorkflowRule.getWorker().registerActivitiesImplementations(activities);
testWorkflowRule.getTestEnvironment().start();
WorkflowClient client = testWorkflowRule.getWorkflowClient();
ApprovalWorkflow workflow = client.newWorkflowStub(
ApprovalWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setWorkflowId("approval-test-002")
.setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
.build()
);
WorkflowClient.start(workflow::processRequest, "request-002");
ApprovalWorkflow workflowById = client.newWorkflowStub(
ApprovalWorkflow.class, "approval-test-002"
);
workflowById.reject("does not meet criteria");
String result = WorkflowStub.fromTyped(workflow).getResult(String.class);
assertEquals("rejected", result);
Mockito.verify(activities, Mockito.never()).executeApproved(Mockito.anyString());
Mockito.verify(activities).executeRejected("request-002", "does not meet criteria");
}
}Testing Activity Retries in Cadence
@Test
public void testActivityRetryOnTransientFailure() {
OrderActivities activities = Mockito.mock(OrderActivities.class);
// Fail twice, succeed on third attempt
Mockito.when(activities.chargeCustomer(Mockito.anyString()))
.thenThrow(new ActivityFailureException("payment gateway timeout"))
.thenThrow(new ActivityFailureException("payment gateway timeout"))
.thenReturn("charge-success");
// Mock other activities to succeed immediately
Mockito.when(activities.validateOrder(Mockito.anyString())).thenReturn(true);
Mockito.when(activities.reserveInventory(Mockito.anyString())).thenReturn("reservation");
Mockito.when(activities.fulfillOrder(Mockito.anyString())).thenReturn("shipped");
testWorkflowRule.getWorker().registerActivitiesImplementations(activities);
testWorkflowRule.getTestEnvironment().start();
OrderWorkflow workflow = testWorkflowRule.getWorkflowClient().newWorkflowStub(
OrderWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList(testWorkflowRule.getTaskList())
.setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
.build()
);
String result = workflow.processOrder("order-retry-test");
assertTrue(result.contains("fulfilled"));
// Should have been called 3 times (2 failures + 1 success)
Mockito.verify(activities, Mockito.times(3)).chargeCustomer("order-retry-test");
}Migrating Cadence Tests to Temporal
When migrating from Cadence to Temporal, the test structure is very similar. Here's a comparison of the key changes:
Package renames:
// Cadence
import com.uber.cadence.testing.TestWorkflowRule;
import com.uber.cadence.client.WorkflowClient;
import com.uber.cadence.workflow.Workflow;
import com.uber.cadence.activity.Activity;
import com.uber.cadence.common.RetryOptions;
// Temporal
import io.temporal.testing.TestWorkflowExtension; // JUnit 5
import io.temporal.testing.TestWorkflowEnvironment; // JUnit 4 style
import io.temporal.client.WorkflowClient;
import io.temporal.workflow.Workflow;
import io.temporal.activity.Activity;
import io.temporal.common.RetryOptions;Test rule vs extension:
// Cadence (JUnit 4 Rule)
@Rule
public TestWorkflowRule testWorkflowRule = TestWorkflowRule.newBuilder()
.setWorkflowTypes(MyWorkflowImpl.class)
.build();
// Temporal (JUnit 5 Extension)
@RegisterExtension
public static final TestWorkflowExtension testWorkflowExtension =
TestWorkflowExtension.newBuilder()
.setWorkflowTypes(MyWorkflowImpl.class)
.setDoNotStart(true)
.build();Workflow options:
// Cadence
WorkflowOptions.newBuilder()
.setTaskList("my-task-list")
.setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
.build()
// Temporal
WorkflowOptions.newBuilder()
.setTaskQueue("my-task-queue") // taskList → taskQueue
.setWorkflowExecutionTimeout(Duration.ofMinutes(5)) // different name
.build()Activity options:
// Cadence
ActivityOptions.Builder()
.setScheduleToCloseTimeout(Duration.ofSeconds(30))
.setRetryOptions(new RetryOptions.Builder()
.setMaximumAttempts(3)
.build())
.build()
// Temporal
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(30)) // recommended in Temporal
.setRetryOptions(RetryOptions.newBuilder()
.setMaximumAttempts(3)
.build())
.build()A migration script to help find-and-replace common patterns:
#!/bin/bash
# Cadence to Temporal Java migration helpers
# Find all files that need migration
find src -name "*.java" -exec grep -l "com.uber.cadence" {} \;
# Replace package imports (run with sed or your IDE's find/replace)
# com.uber.cadence.testing → io.temporal.testing
# com.uber.cadence.client → io.temporal.client
# com.uber.cadence.workflow → io.temporal.workflow
# com.uber.cadence.activity → io.temporal.activity
# com.uber.cadence.common → io.temporal.common
# Find remaining Cadence references after migration
grep -rn "uber.cadence" src/Integration Tests with Docker
For end-to-end tests against a real Cadence server:
@Category(IntegrationTest.class)
public class OrderWorkflowIntegrationTest {
private static WorkflowClient workflowClient;
private static Worker.Factory workerFactory;
@BeforeClass
public static void setUp() {
IWorkflowService service = new WorkflowServiceTChannel(
ClientOptions.newBuilder()
.setHost("localhost")
.setPort(7933)
.build()
);
workflowClient = WorkflowClient.newInstance(service,
WorkflowClientOptions.newBuilder()
.setDomain("integration-test-domain")
.build()
);
workerFactory = new Worker.Factory(service, "integration-test-domain");
Worker worker = workerFactory.newWorker("integration-task-list");
worker.registerWorkflowImplementationTypes(OrderWorkflowImpl.class);
worker.registerActivitiesImplementations(new RealOrderActivities());
workerFactory.start();
}
@AfterClass
public static void tearDown() {
workerFactory.shutdown(Duration.ofSeconds(30));
}
@Test
public void testRealOrderProcessing() {
OrderWorkflow workflow = workflowClient.newWorkflowStub(
OrderWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskList("integration-task-list")
.setExecutionStartToCloseTimeout(Duration.ofMinutes(5))
.build()
);
String result = workflow.processOrder("integration-order-001");
assertNotNull(result);
assertTrue(result.contains("integration-order-001"));
}
}Run with Docker Compose:
version: '3.8'
services:
cadence:
image: ubercadence/server:0.23.2-auto-setup
ports:
- "7933:7933"
- "7934:7934"
- "7935:7935"
- "7939:7939"
environment:
- DB=cassandra
depends_on:
- cassandra
cassandra:
image: cassandra:3.11
ports:
- "9042:9042"Cadence's testing infrastructure is mature and battle-tested at Uber's scale. Whether you're maintaining a Cadence codebase or evaluating migration to Temporal, the patterns covered here—time skipping, activity mocking, signal testing, and determinism validation—apply in both systems with minor syntax differences.