Files
Donald Pinckney 0c8586b4c2 Add Java SDK support (#42)
* Add Java SDK reference files (11 files)

Create complete Java reference documentation covering:
- java.md: Entry point with quick start tutorial, key concepts
- patterns.md: 17 patterns (signals, queries, updates, child workflows,
  saga, cancellation scopes, heartbeating, etc.)
- determinism.md: Safe alternatives table, forbidden operations
- determinism-protection.md: Convention-based enforcement (no sandbox)
- error-handling.md: ApplicationFailure, retry/timeout config
- gotchas.md: Non-deterministic operations, cancellation, heartbeating
- testing.md: TestWorkflowEnvironment, Mockito mocking, replay testing
- versioning.md: Workflow.getVersion(), worker versioning
- data-handling.md: Jackson, PayloadConverter, encryption, search attributes
- observability.md: SLF4J logging, Micrometer metrics
- advanced-features.md: Schedules, async completion, worker tuning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix Java alignment issues from self-review

- Reduce gotchas.md Non-Deterministic Operations from ~94 lines to ~12
  (reference determinism.md instead of duplicating)
- Remove Workflow Failure Exception Types duplication from error-handling.md
  (keep only in advanced-features.md)
- Expand versioning.md Worker Versioning with Key Concepts, PINNED vs
  AUTO_UPGRADE, Deployment Strategies subsections
- Fix section names to match Python reference style:
  Activity Heartbeat Details, Handling Activity Errors,
  Retry Policy Configuration, Workflow Test Environment,
  Mocking Activities, Workflow Replay Testing
- Reduce data-handling.md Payload Encryption verbosity
- Reduce observability.md Logger Customization verbosity
- Reduce testing.md to single approach per section
- Rename determinism.md "Convention-Based Enforcement" to "SDK Protection"
- Fix handler guidance in patterns.md to match Python

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix correctness issues in Java reference files

- patterns.md: Fix Queries section — ActivityStub → typed interface
  (Workflow.newActivityStub returns the typed interface, not ActivityStub)
- data-handling.md: Add missing ProtobufPayloadConverter to default
  converter chain (4th of 5 converters)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add Java to SKILL.md and core/determinism.md

- SKILL.md: Add "Temporal Java" trigger phrase, update Overview to
  list Java, add Java entry to Getting Started references
- core/determinism.md: Add Java entry to SDK Protection Mechanisms
  (no sandbox, convention-based, NonDeterministicException at replay)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Apply manual editorial fixes to Java references

- java.md: Remove "Understanding Replay" section (covered by Overview),
  simplify File Organization note (no sandbox rationale)
- gotchas.md: Move Heartbeating before Cancellation, make Wrong Retry
  Classification brief with reference (not inline examples)
- error-handling.md: Remove editorializing from Workflow Failure note
- determinism-protection.md: Remove cross-language comparison paragraph
  (state Java's approach on its own terms)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add temporal-workflowcheck static analysis to Java determinism docs

- determinism-protection.md: Add "Static Analysis with temporal-workflowcheck"
  section with Gradle/Maven setup, manual run, and suppression instructions.
  Beta warning included.
- determinism.md: Update overview and SDK Protection to reference workflowcheck
- core/determinism.md: Update Java entry in SDK Protection Mechanisms

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Integrate feedback from Go PR into Java patterns

- Updates: Add validator note — validators must not mutate state or
  block (matches note added to Python, TypeScript, Go, and core)
- Saga Pattern: Use Workflow.newDetachedCancellationScope() for
  compensations so they execute even if the workflow is cancelled
  (mirrors Go's workflow.NewDisconnectedContext pattern)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* docs: add @WorkflowInit description to java.md Key Concepts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* mark java as supported

* Apply suggestions from code review

Co-authored-by: Brian Strauch <brian@brianstrauch.com>

* strongly recommend java 21+

* Softened stance on static checker and replay testing.

* address python/typescript sandboxing comment

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Brian Strauch <brian.strauch@temporal.io>
Co-authored-by: Brian Strauch <brian@brianstrauch.com>
2026-04-02 17:07:33 -04:00

5.4 KiB

Java SDK Testing

Overview

You test Temporal Java Workflows using TestWorkflowEnvironment (manual setup) or TestWorkflowExtension (JUnit 5). Activity mocking uses Mockito. The SDK provides WorkflowReplayer for replay-based compatibility testing.

Workflow Test Environment

import io.temporal.testing.TestWorkflowExtension;
import io.temporal.testing.TestWorkflowEnvironment;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.worker.Worker;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyWorkflowTest {

    @RegisterExtension
    public static final TestWorkflowExtension testWorkflowExtension =
        TestWorkflowExtension.newBuilder()
            .setWorkflowTypes(MyWorkflowImpl.class)
            .setDoNotStart(true)
            .build();

    @Test
    void testWorkflow(TestWorkflowEnvironment env, Worker worker, WorkflowClient client) {
        worker.registerActivitiesImplementations(new MyActivitiesImpl());
        env.start();

        MyWorkflow workflow = client.newWorkflowStub(
            MyWorkflow.class,
            WorkflowOptions.newBuilder()
                .setTaskQueue(worker.getTaskQueue())
                .build());

        String result = workflow.run("input");
        assertEquals("expected", result);
    }
}

For manual lifecycle control (e.g., JUnit 4 or custom setups), use TestWorkflowEnvironment directly with @BeforeEach/@AfterEach.

Mocking Activities

import static org.mockito.Mockito.*;

@Test
void testWithMockedActivities(
        TestWorkflowEnvironment env,
        Worker worker,
        WorkflowClient client) {
    // withoutAnnotations() prevents Mockito from copying Temporal annotations
    MyActivities activities = mock(MyActivities.class, withSettings().withoutAnnotations());
    when(activities.composeGreeting("Hello", "World")).thenReturn("mocked result");

    worker.registerActivitiesImplementations(activities);
    env.start();

    MyWorkflow workflow = client.newWorkflowStub(
        MyWorkflow.class,
        WorkflowOptions.newBuilder()
            .setTaskQueue(worker.getTaskQueue())
            .build());

    String result = workflow.run("input");
    assertEquals("mocked result", result);
    verify(activities).composeGreeting("Hello", "World");
}

Testing Signals and Queries

@Test
void testSignalsAndQueries(
        TestWorkflowEnvironment env,
        Worker worker,
        WorkflowClient client) {
    worker.registerActivitiesImplementations(new MyActivitiesImpl());
    env.start();

    MyWorkflow workflow = client.newWorkflowStub(
        MyWorkflow.class,
        WorkflowOptions.newBuilder()
            .setTaskQueue(worker.getTaskQueue())
            .build());

    // Start workflow asynchronously
    WorkflowClient.start(workflow::run, "input");

    // Send signal
    workflow.mySignal("data");

    // Query state
    String status = workflow.getStatus();
    assertEquals("expected", status);

    // Wait for completion
    String result = WorkflowStub.fromTyped(workflow).getResult(String.class);
}

Testing Failure Cases

import io.temporal.client.WorkflowException;

@Test
void testActivityFailure(
        TestWorkflowEnvironment env,
        Worker worker,
        WorkflowClient client) {
    MyActivities activities = mock(MyActivities.class, withSettings().withoutAnnotations());
    when(activities.unreliableAction(anyString()))
        .thenThrow(new RuntimeException("Simulated failure"));

    worker.registerActivitiesImplementations(activities);
    env.start();

    MyWorkflow workflow = client.newWorkflowStub(
        MyWorkflow.class,
        WorkflowOptions.newBuilder()
            .setTaskQueue(worker.getTaskQueue())
            .build());

    assertThrows(WorkflowException.class, () -> workflow.run("input"));
}

Workflow Replay Testing

import io.temporal.testing.WorkflowReplayer;

@Test
void testReplayFromHistory() throws Exception {
    WorkflowReplayer.replayWorkflowExecutionFromResource(
        "my-workflow-history.json",
        MyWorkflowImpl.class);
}

Replay from a WorkflowHistory object:

import io.temporal.common.WorkflowExecutionHistory;

@Test
void testReplayFromJsonString() throws Exception {
    String historyJson = new String(Files.readAllBytes(Paths.get("history.json")));
    WorkflowReplayer.replayWorkflowExecution(
        WorkflowExecutionHistory.fromJson(historyJson),
        MyWorkflowImpl.class);
}

Activity Testing

Activity implementations are plain Java classes. Test them directly:

@Test
void testActivity() {
    MyActivitiesImpl activities = new MyActivitiesImpl();
    String result = activities.composeGreeting("Hello", "World");
    assertEquals("Hello World", result);
}

For activities that use Activity.getExecutionContext() or heartbeating, use TestActivityEnvironment to provide the activity context.

Best Practices

  1. Use TestWorkflowExtension with JUnit 5 for concise test setup
  2. Always use withSettings().withoutAnnotations() when mocking activity interfaces with Mockito
  3. Mock external dependencies in activities, not in workflows
  4. Test replay compatibility when changing workflow code (see references/java/determinism.md)
  5. Test signal/query handlers explicitly
  6. Use unique task queues per test to avoid conflicts (handled automatically by TestWorkflowExtension)