Testing - Java SDK
The Testing section of the Temporal Application development guide describes the frameworks that facilitate Workflow and integration testing.
In the context of Temporal, you can create these types of automated tests:
- End-to-end: Running a Temporal Server and Worker with all its Workflows, Activities, and Nexus Operations; starting and interacting with Workflows from a Client.
- Integration: Anything between end-to-end and unit testing.
- Running Activities with mocked Context and other SDK imports (and usually network requests).
- Running Workers with mock Activities and Nexus Operations, and using a Client to start Workflows.
- Running Workflows with mocked SDK imports.
- Unit: Running a piece of Workflow, Activity, or Nexus Operation code (a function or method) and mocking any code it calls.
We generally recommend writing the majority of your tests as integration tests.
Because the test server supports skipping time, use the test server for both end-to-end and integration tests with Workers.
Test frameworks
The Temporal Java SDK provides a test framework to facilitate Workflow unit and integration testing.
The test framework provides a TestWorkflowEnvironment class which includes an in-memory implementation
of the Temporal service that supports automatic time skipping. This allows you to
easily test long-running Workflows in seconds, without having to change your Workflow code.
You can use the provided TestWorkflowEnvironment with a Java unit testing framework of your choice,
such as JUnit.
Setup testing dependency
To start using the Java SDK test framework, you need to add io.temporal:temporal-testing
as a dependency to your project:
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-testing</artifactId>
<version>1.36.0</version>
<scope>test</scope>
</dependency>
testImplementation ("io.temporal:temporal-testing:1.36.0")
If you need JUnit4 or JUnit5 extensions:
testImplementation("io.temporal:temporal-testing:1.36.0") {
capabilities {
requireCapability("io.temporal:temporal-testing-junit4")
//requireCapability("io.temporal:temporal-testing-junit5")
}
}
Make sure to set the version that matches your dependency version of the Temporal Java SDK.
Test Activities
An Activity can be tested with a mock Activity environment, which provides a way to mock the Activity context, listen to Heartbeats, and cancel the Activity. This behavior allows you to test the Activity in isolation by calling it directly, without needing to create a Worker to run the Activity.
Temporal provides the TestActivityEnvironment and TestActivityExtension classes for testing Activities outside the scope of a Workflow. Testing Activities is similar to testing non-Temporal Java code.
For example, you can test an Activity for:
- Exceptions thrown when invoking the Activity Execution.
- Exceptions thrown when checking for the result of the Activity Execution.
- Activity's return values. Check that the return value matches the expected value.
Here's an example of an Activity that will be referenced in the sections below:
package helloworkflow;
import java.util.concurrent.TimeUnit;
import io.temporal.activity.Activity;
import io.temporal.activity.ActivityExecutionContext;
import io.temporal.client.ActivityCompletionException;
public class GreetActivitiesImpl implements GreetActivities {
@Override
public String greet(String name) {
ActivityExecutionContext context = Activity.getExecutionContext();
for (int i = 0; i < 5; i++) {
try {
context.heartbeat(null);
} catch (ActivityCompletionException e) {
throw e;
}
sleep(1);
}
return "Hello " + name + "!";
}
private void sleep(int seconds) {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(seconds));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
Run an Activity
The following code implements unit tests for the greet Activity above:
package helloworkflow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import io.temporal.testing.TestActivityEnvironment;
public class GreetActivitiesTest {
@Test
public void testActivityImpl() {
TestActivityEnvironment testEnv = TestActivityEnvironment.newInstance();
testEnv.registerActivitiesImplementations(new GreetActivitiesImpl());
GreetActivities activities = testEnv.newActivityStub(GreetActivities.class);
String result = activities.greet("Temporal");
assertEquals("Hello Temporal!", result);
}
}
Listen to Heartbeats
Activities usually issue periodic Heartbeats, a ping that shows that an Activity is making progress and the Worker hasn't crashed. Heartbeats may include details that report task progress in the event an Activity Worker crashes.
When testing Activities that support Heartbeats, make sure you can see those Heartbeats in your test code.
@Test
void testActivityHeartbeat() {
TestActivityEnvironment env = TestActivityEnvironment.newInstance();
AtomicInteger heartbeatCount = new AtomicInteger(0);
env.setActivityHeartbeatListener(
Void.class,
heartbeat -> heartbeatCount.incrementAndGet());
env.registerActivitiesImplementations(new GreetActivitiesImpl());
GreetActivities activities = env.newActivityStub(GreetActivities.class);
String result = activities.greet("Temporal");
assertEquals("Hello Temporal!", result);
assertEquals(5, heartbeatCount.get());
}
Cancel an Activity
Activity cancellation lets Activities know they don't need to continue work and gives time for the Activity to clean up any resources it's created. You can cancel Java-based Activities if they emit Heartbeats. To test an Activity that reacts to Cancellations, make sure that the Activity reacts correctly and cancels.
@Test
void testCancelActivity() {
try (TestWorkflowEnvironment env = TestWorkflowEnvironment.newInstance()) {
Worker worker = env.newWorker(TASK_QUEUE);
worker.registerWorkflowImplementationTypes(SayHelloWorkflowImpl.class);
worker.registerActivitiesImplementations(new GreetActivitiesImpl());
env.start();
SayHelloWorkflow workflow =
env.getWorkflowClient()
.newWorkflowStub(
SayHelloWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue(TASK_QUEUE)
.build());
WorkflowClient.start(workflow::sayHello, "Temporal");
env.registerDelayedCallback(
Duration.ofSeconds(1),
() -> WorkflowStub.fromTyped(workflow).signal("cancelActivity"));
try {
WorkflowStub.fromTyped(workflow).getResult(String.class);
fail("Workflow should have failed because the Activity was canceled");
} catch (WorkflowFailedException e) {
assertEquals(ActivityFailure.class, e.getCause().getClass());
ActivityFailure activityFailure = (ActivityFailure) e.getCause();
assertEquals(CanceledFailure.class, activityFailure.getCause().getClass());
}
}
}
Testing Workflows
How to mock Activities
When integration testing Workflows with a Worker, you can mock Activities by providing mock Activity implementations to the Worker.
In cases where you do not wish to execute your actual Activity or Nexus Operation implementations during unit testing, you can use a framework such as Mockito to mock them.
The following code implements a unit test that shows how Activities can be mocked:
@Test
public void testMockedActivity() {
GreetActivities activities =
mock(GreetActivities.class, withSettings().withoutAnnotations());
when(activities.greet("Temporal")).thenReturn("Hello Temporal!");
assertEquals("Hello Temporal!", activities.greet("Temporal"));
}
Testing with JUnit4
For JUnit4 tests, Temporal provides the TestWorkflowRule class which simplifies the Temporal test environment setup, as well as the creation and shutdown of Workflow Workers in your tests. You can now rewrite the above Activity test class as follows:
public class GreetActivitiesJUnit4Test {
@Rule
public TestWorkflowRule testWorkflowRule =
TestWorkflowRule.newBuilder()
.setWorkflowTypes(SayHelloWorkflowImpl.class)
.setActivityImplementations(new GreetActivitiesImpl())
.build();
@Test
public void testActivityImpl() {
// Get a workflow stub using the same task queue the worker uses.
GreetingWorkflow workflow =
testWorkflowRule
.getWorkflowClient()
.newWorkflowStub(
GreetingWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()
);
// Execute a workflow waiting for it to complete.
String greeting = workflow.sayHello("Temporal");
assertEquals("Hello Temporal!", greeting);
testWorkflowRule.getTestEnvironment().shutdown();
}
}
Testing with JUnit5
For JUnit5 tests, Temporal also provides the TestWorkflowExtension helper class.
This class can be used to simplify the Temporal test environment setup as well as Workflow Worker startup and shutdowns.
To start using JUnit5 TestWorkflowExtension in your tests with Gradle, you need to enable capability [io.temporal:temporal-testing-junit5]:
Now you can use JUnit5 and rewrite the above test class as follows:
public class GreetActivitiesJUnit5Test {
@RegisterExtension
public static final TestWorkflowExtension testWorkflowExtension =
TestWorkflowExtension.newBuilder()
.setWorkflowTypes(SayHelloWorkflowImpl.class)
.setActivityImplementations(new GreetActivitiesImpl())
.build();
@Test
public void testActivityImpl(
TestWorkflowEnvironment testEnv, Worker worker, SayHelloWorkflow workflow) {
// Execute a workflow waiting for it to complete.
String greeting = workflow.sayHello("Temporal");
assertEquals("Hello Temporal!", greeting);
}
}
You can find more unit test examples in the Temporal Java samples repository in its test package.
How to mock Nexus Operations
When integration testing Workflows with a Worker, you can mock Nexus operations by providing mock Nexus Service handlers to the Worker. Alternatively, you could just mock the Nexus service itself.
You can find more example unit tests for Nexus in the Temporal Java samples repository in this test package. These samples show how to call Nexus services in tests using the Temporal testing package and also how to mock them, for both JUnit 4 and 5. Detailed explanatory comments are included in the code in the repository.
To mock Nexus handlers, create a Rule (for JUnit4) or Extension (for JUnit5) from the Temporal testing package and add a call to setNexusServiceImplementation to the builder.
That sets up the Nexus endpoints needed for testing as well as the Nexus handler workflows defined by the Nexus Service implementation.
You will need to create Workers for each handler, using either setWorkflowTypes (for JUnit4) or registerWorkflowImplementationTypes (for JUnit5).
With that in place, you can then mock a Nexus endpoint exactly like any other Workflow like you do with Activity tests.
The following are samples derived from the test package to demonstrate this.
Mocking Nexus handlers with JUnit4
core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowMockTest.java
public class CallerWorkflowMockTest {
@Rule
public TestWorkflowRule testWorkflowRule =
TestWorkflowRule.newBuilder()
.setNexusServiceImplementation(new SampleNexusServiceImpl())
.setWorkflowTypes(HelloCallerWorkflowImpl.class)
.build();
@Test
public void testHelloWorkflow() {
testWorkflowRule
.getWorker()
// Workflows started by a Nexus service can be mocked just like any other workflow
.registerWorkflowImplementationFactory(
HelloHandlerWorkflow.class,
() -> {
HelloHandlerWorkflow wf = mock(HelloHandlerWorkflow.class);
when(wf.hello(any())).thenReturn(new SampleNexusService.HelloOutput("Hello Mock World"));
return wf;
});
// Now create the caller workflow
HelloCallerWorkflow workflow =
testWorkflowRule
.getWorkflowClient()
.newWorkflowStub(
HelloCallerWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build());
String greeting = workflow.hello("World", SampleNexusService.Language.EN);
assertEquals("Hello Mock World", greeting);
}
}
Mocking Nexus handlers with JUnit5
core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5MockTest.java
public class CallerWorkflowJunit5MockTest {
@RegisterExtension
public static final TestWorkflowExtension testWorkflowExtension =
TestWorkflowExtension.newBuilder()
// Register the Nexus service as usual and mock things in the unit tests as needed
.setNexusServiceImplementation(new SampleNexusServiceImpl())
.registerWorkflowImplementationTypes(HelloCallerWorkflowImpl.class)
.build();
@Test
public void testHelloWorkflow(
TestWorkflowEnvironment testEnv, Worker worker, HelloCallerWorkflow workflow) {
// Workflows started by a Nexus service can be mocked just like any other workflow
worker.registerWorkflowImplementationFactory(
HelloHandlerWorkflow.class,
() -> {
HelloHandlerWorkflow mockHandler = mock(HelloHandlerWorkflow.class);
when(mockHandler.hello(any()))
.thenReturn(new SampleNexusService.HelloOutput("Hello Mock World"));
return mockHandler;
});
// Execute a workflow waiting for it to complete.
String greeting = workflow.hello("World", SampleNexusService.Language.EN);
assertEquals("Hello Mock World", greeting);
}
}
An alternative approach is to mock the Nexus service itself, instead of mocking the handlers. This is useful if you want to test the calling logic, but can't easily mock the Nexus handlers.
The code will mock the implementation of the SampleNexusService class with the handler methods, but will need those methods stubbed in for the testing framework.
Those methods can be directly mocked with static return values or they can return an instance variable which each unit test can modify to return a desired value.
Mocking the Nexus Service with JUnit4
core/src/test/java/io/temporal/samples/nexus/caller/NexusServiceMockTest.java
public class NexusServiceMockTest {
private final SampleNexusService mockNexusService = mock(SampleNexusService.class);
/**
* A test-only Nexus service implementation that delegates to the Mockito mock defined above. The
* operation is implemented as a synchronous handler that forward calls to the mock, allowing
* full control over return values and verification of inputs.
*/
@ServiceImpl(service = SampleNexusService.class)
public class TestNexusServiceImpl {
@OperationImpl
@SuppressWarnings("DirectInvocationOnMock")
public OperationHandler<SampleNexusService.HelloInput, SampleNexusService.HelloOutput> hello() {
return OperationHandler.sync((ctx, details, input) -> mockNexusService.hello(input));
}
}
// Using OperationHandler.sync for the operation bypasses the need for a backing workflow,
// returning results inline just like a synchronous call.
@Rule
public TestWorkflowRule testWorkflowRule =
TestWorkflowRule.newBuilder()
.setNexusServiceImplementation(new TestNexusServiceImpl())
.setWorkflowTypes(HelloCallerWorkflowImpl.class)
.build();
@Test
public void testHelloCallerWithMockedService() {
when(mockNexusService.hello(any()))
.thenReturn(new SampleNexusService.HelloOutput("Bonjour World"));
HelloCallerWorkflow workflow =
testWorkflowRule
.getWorkflowClient()
.newWorkflowStub(
HelloCallerWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build());
String result = workflow.hello("World", SampleNexusService.Language.FR);
assertEquals("Bonjour World", result);
// Verify the Nexus service was called with the correct name and language
verify(mockNexusService)
.hello(
argThat(
input ->
"World".equals(input.getName())
&& SampleNexusService.Language.FR == input.getLanguage()));
// Verify the operation was called exactly once and no other operations were invoked
verify(mockNexusService, times(1)).hello(any());
}
}
Mocking the Nexus Service with JUnit5
core/src/test/java/io/temporal/samples/nexus/caller/NexusServiceJunit5Test.java
public class NexusServiceJunit5Test {
private final SampleNexusService mockNexusService = mock(SampleNexusService.class);
/**
* A test-only Nexus service implementation that delegates to the Mockito mock defined above. The
* operation is implemented as a synchronous handler that forward calls to the mock, allowing
* full control over return values and verification of inputs.
*/
@ServiceImpl(service = SampleNexusService.class)
public class TestNexusServiceImpl {
@OperationImpl
@SuppressWarnings("DirectInvocationOnMock")
public OperationHandler<SampleNexusService.HelloInput, SampleNexusService.HelloOutput> hello() {
return OperationHandler.sync((ctx, details, input) -> mockNexusService.hello(input));
}
}
// Using OperationHandler.sync for both operations bypasses the need for a backing workflow,
// returning results inline just like a synchronous call.
@RegisterExtension
public final TestWorkflowExtension testWorkflowExtension =
TestWorkflowExtension.newBuilder()
// If a Nexus service is registered as part of the test as in the following line of code,
// the TestWorkflowExtension will, by default, automatically create a Nexus service
// endpoint and workflows registered as part of the TestWorkflowExtension will
// automatically inherit the endpoint if none is set.
.setNexusServiceImplementation(new TestNexusServiceImpl())
// registerWorkflowImplementationTypes will take the classes given and create workers for
// them, enabling workflows to run.
// Since both operations are mocked with OperationHandler.sync, no backing workflow is
// needed for hello — only the caller workflow types need to be registered.
.registerWorkflowImplementationTypes(HelloCallerWorkflowImpl.class)
// The workflow will start before each test, and will shut down after each test.
// See CallerWorkflowTest for an example of how to control this differently if needed.
.build();
// The TestWorkflowExtension extension in the Temporal testing library creates the
// arguments to the test cases and initializes them from the extension setup call above.
@Test
public void testHelloWorkflow(
TestWorkflowEnvironment testEnv, Worker worker, HelloCallerWorkflow workflow) {
// Set the mock value to return
when(mockNexusService.hello(any()))
.thenReturn(new SampleNexusService.HelloOutput("Hello Mock World"));
// Execute a workflow waiting for it to complete.
String greeting = workflow.hello("World", SampleNexusService.Language.EN);
assertEquals("Hello Mock World", greeting);
// Verify the operation was called exactly once and no other operations were invoked
verify(mockNexusService, times(1)).hello(any());
// Verify the Nexus service was called with the correct input
verify(mockNexusService).hello(argThat(input -> "World".equals(input.getName())));
verifyNoMoreInteractions(mockNexusService);
}
}
How to skip time
Some long-running Workflows can persist for months or even years. Implementing the test framework allows your Workflow code to skip time and complete your tests in seconds rather than the Workflow's specified amount.
For example, if you have a Workflow sleep for a day, or have an Activity failure with a long retry interval, you don't need to wait the entire length of the sleep period to test whether the sleep function works. Instead, test the logic that happens after the sleep by skipping forward in time and complete your tests in a timely manner.
The test framework included in most SDKs is an in-memory implementation of Temporal Server that supports skipping time.
Time is a global property of an instance of TestWorkflowEnvironment: skipping time (either automatically or manually) applies to all currently running tests.
If you need different time behaviors for different tests, run your tests in a series or with separate instances of the test server.
For example, you could run all tests with automatic time skipping in parallel, and then all tests with manual time skipping in series, and then all tests without time skipping in parallel.
Skip time automatically
When you execute a Workflow and wait for the result, the test environment automatically skips Workflow timers such as Workflow.sleep. This means:
- Workflow timers (like
Workflow.Sleep) are fast-forwarded. - Time doesn't skip while Activities and Nexus operations are executing.
@Test
void testSleepCompletesWithoutWaitingOneDay() {
try (TestWorkflowEnvironment testEnv = TestWorkflowEnvironment.newInstance()) {
Worker worker = testEnv.newWorker(TASK_QUEUE);
worker.registerWorkflowImplementationTypes(SayHelloWorkflowImpl.class);
worker.registerActivitiesImplementations(new GreetActivitiesImpl());
testEnv.start();
SayHelloWorkflow workflow =
testEnv
.getWorkflowClient()
.newWorkflowStub(
SayHelloWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());
String result = workflow.sayHello("Temporal");
assertEquals("Hello Temporal!", result);
}
}
Use Workflow.sleep() in Workflow code, not Thread.sleep(). Workflow.sleep() creates a Temporal timer that the test environment can skip. Thread.sleep() blocks a Java thread in real time and is not controlled by Temporal’s time-skipping test server.
Skip time manually
Use TestWorkflowEnvironment.sleep(Duration) when you want to advance virtual time yourself and inspect intermediate Workflow state. Start the Workflow asynchronously with WorkflowClient.start(), then call testEnv.sleep() from the test.
Here's an example of a Workflow test that lets you manually advance time with testEnv.sleep():
package helloworkflow;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowOptions;
import io.temporal.testing.TestWorkflowEnvironment;
import io.temporal.worker.Worker;
import io.temporal.workflow.QueryMethod;
import io.temporal.workflow.Workflow;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;
public class ManualTimeSkippingTest {
private static final String TASK_QUEUE = "manual-time-skipping-test";
@WorkflowInterface
public interface ProgressWorkflow {
@WorkflowMethod
void run();
@QueryMethod
int daysElapsed();
}
public static class ProgressWorkflowImpl implements ProgressWorkflow {
private int daysElapsed = 0;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Workflow.sleep(Duration.ofDays(1));
daysElapsed++;
}
}
@Override
public int daysElapsed() {
return daysElapsed;
}
}
@Test
void manuallyAdvanceWorkflowTime() {
try (TestWorkflowEnvironment testEnv = TestWorkflowEnvironment.newInstance()) {
Worker worker = testEnv.newWorker(TASK_QUEUE);
worker.registerWorkflowImplementationTypes(ProgressWorkflowImpl.class);
testEnv.start();
ProgressWorkflow workflow =
testEnv
.getWorkflowClient()
.newWorkflowStub(
ProgressWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(TASK_QUEUE).build());
WorkflowClient.start(workflow::run);
assertEquals(0, workflow.daysElapsed());
testEnv.sleep(Duration.ofHours(25));
assertEquals(1, workflow.daysElapsed());
testEnv.sleep(Duration.ofHours(25));
assertEquals(2, workflow.daysElapsed());
}
}
}
How to Replay a Workflow Execution
Replay recreates the exact state of a Workflow Execution. You can replay a Workflow from the beginning of its Event History.
Replay succeeds only if the Workflow Definition is compatible with the provided history from a deterministic point of view.
When you test changes to your Workflow Definitions, we recommend doing the following as part of your CI checks:
- Determine which Workflow Types or Task Queues (or both) will be targeted by the Worker code under test.
- Download the Event Histories of a representative set of recent open and closed Workflows from each Task Queue, either programmatically using the SDK client or via the Temporal CLI.
- Run the Event Histories through replay.
- Fail CI if any error is encountered during replay.
The following are examples of fetching and replaying Event Histories:
To replay Workflow Executions, use the WorkflowReplayer class in the temporal-testing package.
In the following example, Event Histories are downloaded from the server, and then replayed. Note that this requires Advanced Visibility to be enabled.
// Note we assume you already have a WorkflowServiceStubs (`service`) and WorkflowClient (`client`)
// in scope.
ListWorkflowExecutionsRequest listWorkflowExecutionRequest =
ListWorkflowExecutionsRequest.newBuilder()
.setNamespace(client.getOptions().getNamespace())
.setQuery("TaskQueue = 'mytaskqueue'")
.build();
ListWorkflowExecutionsResponse listWorkflowExecutionsResponse =
service.blockingStub().listWorkflowExecutions(listWorkflowExecutionRequest);
List<WorkflowExecutionHistory> histories =
listWorkflowExecutionsResponse.getExecutionsList().stream()
.map(
(info) -> {
GetWorkflowExecutionHistoryResponse weh =
service.blockingStub().getWorkflowExecutionHistory(
GetWorkflowExecutionHistoryRequest.newBuilder()
.setNamespace(testEnvironment.getNamespace())
.setExecution(info.getExecution())
.build());
return new WorkflowExecutionHistory(
weh.getHistory(), info.getExecution().getWorkflowId());
})
.collect(Collectors.toList());
WorkflowReplayer.replayWorkflowExecutions(
histories, true, WorkflowA.class, WorkflowB.class, WorkflowC.class);
In the next example, a single history is loaded from a JSON file on disk:
File file = new File("my_history.json");
WorkflowReplayer.replayWorkflowExecution(file, MyWorkflow.class);
In both examples, if Event History is non-deterministic, an error is thrown.
You can choose to wait until all histories have been replayed with replayWorkflowExecutions by setting the failFast argument to false.