This guide covers testing patterns and best practices for Temporal workflows in Java.
- Testing Overview
- Unit Testing Activities
- Integration Testing Workflows
- Test Configuration
- Time-Skipping Tests
- Mocking Patterns
- Coverage Requirements
- Running Tests
This template uses a layered testing approach:
- Unit Tests - Test activities in isolation with mocked dependencies
- Integration Tests - Test workflows end-to-end with mocked activities
- Coverage - Minimum 80% code coverage enforced by JaCoCo
- JUnit 5 - Test framework
- Mockito - Mocking framework for dependencies
- Temporal TestWorkflowEnvironment - In-memory workflow testing
- JaCoCo - Code coverage reporting
Activities should be tested in isolation with mocked dependencies.
@ExtendWith(MockitoExtension.class)
class HttpActivitiesTest {
@Mock
private RestTemplate restTemplate;
private HttpActivitiesImpl activities;
@BeforeEach
void setUp() {
activities = new HttpActivitiesImpl(restTemplate);
}
@Test
void testHttpGet_Success() {
// Arrange
String url = "https://example.com";
String responseBody = "<html>Content</html>";
ResponseEntity<String> responseEntity =
new ResponseEntity<>(responseBody, HttpStatus.OK);
when(restTemplate.getForEntity(url, String.class))
.thenReturn(responseEntity);
// Act
HttpGetActivityInput input = new HttpGetActivityInput(url);
HttpGetActivityOutput output = activities.httpGet(input);
// Assert
assertNotNull(output);
assertEquals(responseBody, output.responseText());
assertEquals(200, output.statusCode());
}
@Test
void testHttpGet_NetworkError() {
// Arrange
String url = "https://invalid.com";
when(restTemplate.getForEntity(url, String.class))
.thenThrow(new RestClientException("Connection refused"));
// Act & Assert
HttpGetActivityInput input = new HttpGetActivityInput(url);
assertThrows(RestClientException.class,
() -> activities.httpGet(input));
}
}- Use @ExtendWith(MockitoExtension.class) for Mockito support
- Mock external dependencies (HTTP clients, databases, etc.)
- Test success paths and error conditions
- Verify behavior with assertions
- Test edge cases (null responses, empty data, etc.)
Workflows should be tested end-to-end using TestWorkflowEnvironment.
class HttpWorkflowTest {
private TestWorkflowEnvironment testEnv;
private Worker worker;
private WorkflowClient client;
@BeforeEach
void setUp() {
testEnv = TestWorkflowEnvironment.newInstance();
worker = testEnv.newWorker(HttpWorker.TASK_QUEUE);
worker.registerWorkflowImplementationTypes(HttpWorkflowImpl.class);
client = testEnv.getWorkflowClient();
}
@AfterEach
void tearDown() {
testEnv.close();
}
@Test
void testHttpWorkflow_Success() {
// Arrange: Mock activities
HttpActivities mockActivities = mock(HttpActivities.class);
worker.registerActivitiesImplementations(mockActivities);
String testUrl = "https://example.com";
HttpGetActivityOutput activityOutput =
new HttpGetActivityOutput("Response", 200);
when(mockActivities.httpGet(any(HttpGetActivityInput.class)))
.thenReturn(activityOutput);
testEnv.start();
// Act: Execute workflow
HttpWorkflow workflow = client.newWorkflowStub(
HttpWorkflow.class,
WorkflowOptions.newBuilder()
.setTaskQueue(HttpWorker.TASK_QUEUE)
.build());
HttpWorkflowInput input = new HttpWorkflowInput(testUrl);
HttpWorkflowOutput output = workflow.run(input);
// Assert
assertNotNull(output);
assertEquals("Response", output.responseText());
assertEquals(testUrl, output.url());
assertEquals(200, output.statusCode());
// Verify activity was called
verify(mockActivities).httpGet(any(HttpGetActivityInput.class));
}
}- Create TestWorkflowEnvironment in @BeforeEach
- Register workflow implementations to the worker
- Mock activities to isolate workflow logic
- Start test environment before execution
- Close environment in @AfterEach
- Verify activity interactions with Mockito
Create a test configuration for reusable test beans:
@TestConfiguration
public class TestConfig {
@Bean
public TestWorkflowEnvironment testWorkflowEnvironment() {
return TestWorkflowEnvironment.newInstance();
}
@Bean
public WorkflowClient testWorkflowClient(TestWorkflowEnvironment testEnv) {
return testEnv.getWorkflowClient();
}
}For tests that need Spring context:
@SpringBootTest
@Import(TestConfig.class)
class MyIntegrationTest {
@Autowired
private TestWorkflowEnvironment testEnv;
@Autowired
private WorkflowClient client;
// Tests...
}TestWorkflowEnvironment supports time-skipping for testing workflows with timers.
@Test
void testWorkflowWithDelay() {
// Register workflow and activities
worker.registerWorkflowImplementationTypes(MyWorkflowImpl.class);
worker.registerActivitiesImplementations(mockActivities);
testEnv.start();
// Execute workflow asynchronously
WorkflowClient.start(workflow::run, input);
// Skip forward in time (no actual waiting)
testEnv.sleep(Duration.ofHours(1));
// Verify workflow continued after sleep
String result = workflow.run(input);
assertEquals("expected", result);
}Time-skipping also works with retry intervals:
@Test
void testActivityRetry() {
// Configure activity to fail then succeed
when(mockActivities.unstableOperation())
.thenThrow(new RuntimeException("Temporary failure"))
.thenReturn("Success");
testEnv.start();
// Execute workflow - will retry activity
String result = workflow.run();
// Verify retry happened
verify(mockActivities, times(2)).unstableOperation();
assertEquals("Success", result);
}@Test
void testCrawlerWorkflow_MultiplePages() {
CrawlerActivities mockActivities = mock(CrawlerActivities.class);
// First call returns 2 links
when(mockActivities.parseLinksFromUrl(
new ParseLinksInput("https://page1.com")))
.thenReturn(new ParseLinksOutput(
List.of("https://page2.com", "https://page3.com")));
// Subsequent calls return empty
when(mockActivities.parseLinksFromUrl(
new ParseLinksInput("https://page2.com")))
.thenReturn(new ParseLinksOutput(List.of()));
when(mockActivities.parseLinksFromUrl(
new ParseLinksInput("https://page3.com")))
.thenReturn(new ParseLinksOutput(List.of()));
worker.registerActivitiesImplementations(mockActivities);
testEnv.start();
// Test workflow behavior with multiple pages
CrawlerWorkflowOutput output = workflow.run(input);
assertEquals(3, output.totalLinksCrawled());
}// Match any input
when(mockActivities.process(any(Input.class)))
.thenReturn(new Output("result"));
// Match specific values
when(mockActivities.process(argThat(input ->
input.url().contains("example.com"))))
.thenReturn(new Output("example result"));
// Verify with matchers
verify(mockActivities).process(argThat(input ->
input.retries() > 0));@Test
void testActivityArguments() {
ArgumentCaptor<ParseLinksInput> captor =
ArgumentCaptor.forClass(ParseLinksInput.class);
workflow.run(input);
verify(mockActivities, atLeastOnce())
.parseLinksFromUrl(captor.capture());
List<ParseLinksInput> allInputs = captor.getAllValues();
assertTrue(allInputs.stream()
.anyMatch(i -> i.url().contains("example.com")));
}This template enforces 80% minimum coverage:
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80
}
}
}
}# Generate coverage report
./gradlew test jacocoTestReport
# View HTML report
open build/reports/jacoco/test/html/index.htmlThe report shows coverage by:
- Instructions - Bytecode instructions executed
- Branches - Decision points (if/else, switch, etc.)
- Lines - Source code lines
- Methods - Method coverage
- Classes - Class coverage
Focus on:
- Test all public methods in activities and workflows
- Test error paths and exception handling
- Test edge cases (empty inputs, null values, etc.)
- Test conditional logic (all branches)
- Test different input combinations
# Run all tests
./gradlew test
# Run tests for specific package
./gradlew test --tests "com.example.temporal.workflows.http.*"
# Run specific test class
./gradlew test --tests HttpWorkflowTest
# Run specific test method
./gradlew test --tests HttpWorkflowTest.testHttpWorkflow_Success
# Run with coverage
./gradlew test jacocoTestReport
# Continuous testing (watch mode)
./gradlew test --continuousIntelliJ IDEA:
- Right-click test class/method → Run
- Run with coverage → Run with Coverage
- View coverage in editor gutter
VS Code:
- Install "Test Runner for Java" extension
- Click run/debug above test methods
- View coverage with "Coverage Gutters" extension
src/test/java/com/example/temporal/
├── TestConfig.java # Shared test configuration
└── workflows/
├── http/
│ ├── HttpActivitiesTest.java # Activity unit tests
│ └── HttpWorkflowTest.java # Workflow integration tests
└── crawler/
├── CrawlerActivitiesTest.java # Activity unit tests
└── CrawlerWorkflowTest.java # Workflow integration tests
- Test Classes:
<ClassName>Test.java - Test Methods:
test<Method>_<Scenario>()(underscores allowed) - Mocks:
mock<Type>(e.g.,mockActivities)
- Test both success and failure scenarios
- Mock external dependencies (HTTP, DB, etc.)
- Use TestWorkflowEnvironment for workflow tests
- Verify activity interactions with
verify() - Clean up resources in @AfterEach
- Use meaningful test method names
- Test edge cases and boundary conditions
- Maintain 80%+ code coverage
- Test with real external services
- Skip cleanup in @AfterEach
- Test implementation details
- Write tests that depend on execution order
- Ignore test failures in CI
- Mock everything (test real workflow logic)
- Write tests without assertions
# src/test/resources/application-test.yml
logging:
level:
io.temporal: DEBUG
com.example.temporal: DEBUG@Test
void debugWorkflow() {
workflow.run(input);
// Print workflow history for debugging
System.out.println(
testEnv.getWorkflowClient()
.fetchHistory("workflow-id")
.getHistory());
}Use IDE debugger to:
- Set breakpoints in test methods
- Step through workflow execution
- Inspect workflow state
- Examine activity calls