Quarkus Temporal Test
A Quarkus extension that lets you mock Temporal for testing.
Installation
If you want to use this extension, you need to add the io.quarkiverse.temporal:quarkus-temporal-test extension first to your build file.
For instance, with Maven, add the following dependency to your POM file:
<dependency>
<groupId>io.quarkiverse.temporal</groupId>
<artifactId>quarkus-temporal-test</artifactId>
<version>0.6.0</version>
<scope>test</scope>
</dependency>
How It Works
Enabling the mock environment swaps the real Temporal connection for Temporal’s in-memory TestWorkflowEnvironment (the same one used by the standalone temporal-testing suite):
-
The
WorkflowClientCDI bean is backed by the in-memory test environment, so workflow stubs (created withWorkflowClient.newWorkflowStubor injected with@TemporalWorkflowStub) talk to the in-memory server instead of a real Temporal cluster. -
The
WorkerFactoryCDI bean is backed by the in-memory test environment. The extension still creates a default worker when needed, and workflow implementations are automatically registered with the worker(s) they are bound to. -
You can also inject a typed workflow stub directly using
@TemporalWorkflowStub, which reduces test boilerplate. If the workflow is bound to multiple workers, specify the worker in the annotation. -
The
TestWorkflowEnvironmentCDI bean exposes the underlying in-memory test environment. Tests can inject it to use Temporal testing-suite features, such as time skipping withskipForward(…). -
When
quarkus.temporal.start-workers=false, Activity implementations are not registered automatically. The test is responsible for registering every activity the workflow invokes, either as a mock or as the real CDI bean.
|
|
Getting Started
Enable the mock environment for the test profile. When you register mock activities in the test, also disable automatic worker startup so the test controls when the worker factory starts (it must happen after the mocks are registered):
%test.quarkus.temporal.enable-mock=true (1)
%test.quarkus.temporal.start-workers=false (2)
%test.quarkus.devservices.enabled=false (3)
| 1 | Switches the extension to the in-memory test environment. |
| 2 | Defers WorkerFactory.start() to the test, which runs it after registering the mock activities. |
| 3 | Disables Quarkus Dev Services (test containers) when you don’t need them. |
A @QuarkusTest can then inject the Logger, WorkflowClient, WorkerFactory, and a typed workflow stub injected with @TemporalWorkflowStub, all backed by the test environment:
@QuarkusTest
public class OrderEntityWorkflowTest {
@Inject
Logger log;
@Inject
WorkflowClient workflowClient;
@Inject
WorkerFactory workerFactory;
@Inject
@TemporalWorkflowStub
OrderEntityWorkflow workflow;
}
Writing a Workflow Test
The pattern is: create a mock for each activity interface, stub its behavior, register the mocks with the worker that owns them, start the worker factory (after registering the activities), drive the workflow (start + signals), block on the result, and assert.
The following is a complete @QuarkusTest that exercises a signal-driven workflow end to end against the in-memory test environment:
@QuarkusTest
public class QuarkusBasedOrderEntityWorkflowTest {
static final String customerId = "Mike";
@ConfigProperty(name = "quarkus.temporal.worker.task-queue")
String taskQueue;
@ConfigProperty(name = "quarkus.temporal.namespace")
String workflowNamespace;
@Inject
WorkflowClient workflowClient;
@Inject
WorkerFactory workerFactory;
@Inject
@TemporalWorkflowStub
OrderEntityWorkflow workflow;
@Test
public void testHappyPathOrderProcessing() {
// Create a mock for each activity interface you want to control.(1)
LocalConfigActivities mockLocalConfigActivities = mock(LocalConfigActivities.class, withSettings().withoutAnnotations());
OrderValidationActivities mockOrderValidationActivities = mock(OrderValidationActivities.class, withSettings().withoutAnnotations());
// Create test data
OrderInit orderInit = createOrderInit();
OrderInput orderInput = createValidOrder();
// Stub the behavior each activity should exhibit for this test.(2)
OrderEntityConfig config = new OrderEntityConfig(
Duration.ofSeconds(5),
Duration.ofMillis(100)
);
when(mockLocalConfigActivities.getEntityConfig()).thenReturn(config);
when(mockOrderValidationActivities.validateOrder(orderInput))
.thenReturn(new OrderValidationResponse(true));
// Register the mocks with the worker that owns each activity.(3)
workerFactory.getWorker(taskQueue)
.registerActivitiesImplementations(
mockLocalConfigActivities,
mockOrderValidationActivities
);
// Start the workers only AFTER the activities have been registered.(4)
workerFactory.start();
try {
// Start the workflow asynchronously.(5)
WorkflowClient.start(workflow::create, orderInit);
// Send signals through the typed stub.(6)
workflow.orderInput(orderInput);
// Block on the workflow result and assert.(7)
String result = WorkflowStub.fromTyped(workflow).getResult(String.class);
assertNotNull(result, "Workflow result should not be null");
assertTrue(result.contains("completed"), "Result should indicate completion");
assertTrue(result.contains(customerId), "Result should contain customer ID");
assertTrue(result.contains(orderInit.orderId()), "Result should contain order ID");
assertTrue(result.contains("order canceled false"), "Order should not be canceled");
assertTrue(result.contains("order expired false"), "Order should not be expired");
assertTrue(result.contains("order status FULFILLMENT"), "Order status should be FULFILLMENT");
} finally {
// Clean up.(8)
workerFactory.shutdown();
}
}
private OrderInit createOrderInit() {
return new OrderInit(
UUID.randomUUID().toString(),
customerId,
"xyzzy"
);
}
private OrderInput createValidOrder() {
OrderInputItem item = new OrderInputItem("baseball", 1);
ArrayList<OrderInputItem> items = new ArrayList<>();
items.add(item);
return new OrderInput(
UUID.randomUUID().toString(),
customerId,
new OrderInputOrder(
"xyzzy",
items
)
);
}
}
| 1 | Create a mock for each activity interface you want to control. |
| 2 | Stub the behavior each activity should exhibit for this test. |
| 3 | Register the mocks with the worker that owns each activity. Every activity the workflow invokes must be registered (mock or real) because none are registered automatically when mocking is enabled. |
| 4 | Start the workers only after the activities have been registered. |
| 5 | Start the workflow asynchronously. |
| 6 | Send signals through the typed stub. |
| 7 | Block on the workflow result and assert. |
| 8 | Shut the worker factory down when the test is done. |
|
To keep the real implementation of an activity instead of mocking it, register the CDI bean directly:
|
Injecting TestWorkflowEnvironment Directly
Inject TestWorkflowEnvironment directly when you need access to features that are not available through WorkflowClient or WorkerFactory, such as creating separate workers for testing other workflows or activities.
In this mode, the test environment already provides access to both WorkflowClient and WorkerFactory, so you do not need to inject them as separate CDI beans. Explicit injection is still supported if you prefer a more explicit setup.
Start workers by calling testEnv.start() rather than workerFactory.start() so that the test environment coordinates the worker lifecycle correctly. Unlike the WorkerFactory-based pattern shown earlier, you do not need to call shutdown() explicitly; the TestWorkflowEnvironment cleans up automatically when the test completes.