Cadence Workflow Testing Patterns: From Unit to Integration

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() or UUID.randomUUID() directly
  • Calling System.currentTimeMillis() or new 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-id

Testing 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.

Read more

Start now free