Files
Devin Smaldore 8d05f55a5d Add Spring Boot integration reference for Java SDK (#74)
Covers auto-discovery mechanics, annotation layering (@WorkflowImpl vs
@ActivityImpl + @Component), WorkflowClient injection, worker lifecycle,
testing strategies, and Spring-specific gotchas. Updates java.md and
testing.md with pointers to the new reference.

Co-authored-by: Donald Pinckney <donald.pinckney@temporal.io>
2026-04-30 13:21:48 -04:00

256 lines
7.8 KiB
Markdown

# 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
```java
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
```java
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
```java
@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
```java
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
```java
import io.temporal.testing.WorkflowReplayer;
@Test
void testReplayFromHistory() throws Exception {
WorkflowReplayer.replayWorkflowExecutionFromResource(
"my-workflow-history.json",
MyWorkflowImpl.class);
}
```
Replay from a `WorkflowHistory` object:
```java
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:
```java
@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`)
## Spring Boot Testing
Two strategies — choose one per test class, do not mix them.
### Embedded test server in Spring context
For full integration tests that exercise the Spring context (DB, beans, config):
```properties
# src/test/resources/application-test.properties
spring.temporal.test-server.enabled=true
```
```java
@SpringBootTest
@ActiveProfiles("test")
class TeeTimeMonitorIntegrationTest {
@Autowired
WorkflowClient client; // auto-configured to point at the embedded test server
@Test
void testWorkflow() {
var stub = client.newWorkflowStub(
TeeTimeMonitorWorkflow.class,
WorkflowOptions.newBuilder()
.setWorkflowId("test-" + UUID.randomUUID())
.setTaskQueue("golfnow")
.build()
);
var result = stub.monitorTeeTimes(new TTMonitorRequest(...));
assertNotNull(result);
}
}
```
The embedded server does not support time-skipping. Use this when you need Spring beans (real DB, email service, etc.) wired alongside Temporal.
### Unit tests without Spring context
For faster, isolated tests with time-skipping support, use `TestWorkflowExtension` or `TestWorkflowEnvironment` directly. No Spring context starts, so activity dependencies must be provided manually (real instances or Mockito mocks):
```java
public class TeeTimeMonitorWorkflowTest {
@RegisterExtension
static final TestWorkflowExtension testWorkflow = TestWorkflowExtension.newBuilder()
.setWorkflowTypes(TeeTimeMonitorWorkflowImpl.class)
.setDoNotStart(true)
.build();
@Test
void testWorkflow(TestWorkflowEnvironment env, Worker worker, WorkflowClient client) {
GolfNowActivities activities = mock(GolfNowActivities.class, withSettings().withoutAnnotations());
when(activities.searchTeeTimes(any())).thenReturn(List.of());
worker.registerActivitiesImplementations(activities);
env.start();
var stub = client.newWorkflowStub(
TeeTimeMonitorWorkflow.class,
WorkflowOptions.newBuilder().setTaskQueue(worker.getTaskQueue()).build()
);
stub.monitorTeeTimes(new TTMonitorRequest(...));
verify(activities).searchTeeTimes(any());
}
}
```
See the sections above for more detail on mocking, signals/queries, and replay testing.