From 2cef2dffa134548231443c1a0bb252d32528f96f Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 26 Mar 2026 09:59:57 -0700 Subject: [PATCH 1/2] Updating Java Nexus docs with testing samples --- docs/develop/java/temporal-nexus.mdx | 64 ++++---- docs/develop/java/testing-suite.mdx | 220 ++++++++++++++++++++++++++- 2 files changed, 250 insertions(+), 34 deletions(-) diff --git a/docs/develop/java/temporal-nexus.mdx b/docs/develop/java/temporal-nexus.mdx index 10b16bee12..fc1377912f 100644 --- a/docs/develop/java/temporal-nexus.mdx +++ b/docs/develop/java/temporal-nexus.mdx @@ -96,10 +96,10 @@ In a polyglot environment, that is where more than one language and SDK is being This example uses Java classes serialized into JSON. -[core/src/main/java/io/temporal/samples/nexus/service/NexusService.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexus/service/NexusService.java) +[core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexus/service/SampleNexusService.java) ```java @Service -public interface NexusService { +public interface SampleNexusService { enum Language { EN, FR, @@ -203,15 +203,15 @@ Use `Nexus.getOperationContext().getWorkflowClient(ctx)` to get the Temporal Cli Implementations can also make other calls, but handlers should be reliable to avoid tripping the [circuit breaker](/nexus/operations#circuit-breaking). {/* SNIPSTART samples-java-nexus-handler {"selectedLines": ["1-16", "43"]} */} -[core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java) +[core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java) ```java // To create a service implementation, annotate the class with @ServiceImpl and provide the // interface that the service implements. The service implementation class should have methods that // return OperationHandler that correspond to the operations defined in the service interface. -@ServiceImpl(service = NexusService.class) -public class NexusServiceImpl { +@ServiceImpl(service = SampleNexusService.class) +public class SampleNexusServiceImpl { @OperationImpl - public OperationHandler echo() { + public OperationHandler echo() { // OperationHandler.sync is a meant for exposing simple RPC handlers. return OperationHandler.sync( // The method is for making arbitrary short calls to other services or databases, or @@ -219,7 +219,7 @@ public class NexusServiceImpl { // calling // Nexus.getOperationContext().getWorkflowClient(ctx) to make arbitrary calls such as // signaling, querying, or listing workflows. - (ctx, details, input) -> new NexusService.EchoOutput(input.getMessage())); + (ctx, details, input) -> new SampleNexusService.EchoOutput(input.getMessage())); } // ... } @@ -237,16 +237,16 @@ All calls must complete within the [Nexus request timeout](/cloud/limits#nexus-o Use the `WorkflowRunOperation.fromWorkflowMethod` method, which is the easiest way to expose a Workflow as an operation. -[core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java) +[core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexus/handler/SampleNexusServiceImpl.java) ```java // To create a service implementation, annotate the class with @ServiceImpl and provide the // interface that the service implements. The service implementation class should have methods that // return OperationHandler that correspond to the operations defined in the service interface. -@ServiceImpl(service = NexusService.class) -public class NexusServiceImpl { +@ServiceImpl(service = SampleNexusService.class) +public class SampleNexusServiceImpl { // ... @OperationImpl - public OperationHandler hello() { + public OperationHandler hello() { // Use the WorkflowRunOperation.fromWorkflowMethod constructor, which is the easiest // way to expose a workflow as an operation. To expose a workflow with a different input // parameters then the operation or from an untyped stub, use the @@ -284,15 +284,15 @@ Workflow IDs should typically be business-meaningful IDs and are used to dedupe A Nexus Operation can only take one input parameter. If you want a Nexus Operation to start a Workflow that takes multiple arguments use the `WorkflowRunOperation.fromWorkflowHandle` method. -[core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/NexusServiceImpl.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/NexusServiceImpl.java) +[core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/SampleNexusServiceImpl.java](https://github.com/temporalio/samples-java/blob/nexus-snip-sync/core/src/main/java/io/temporal/samples/nexusmultipleargs/handler/SampleNexusServiceImpl.java) ```java // To create a service implementation, annotate the class with @ServiceImpl and provide the // interface that the service implements. The service implementation class should have methods that // return OperationHandler that correspond to the operations defined in the service interface. -@ServiceImpl(service = NexusService.class) -public class NexusServiceImpl { +@ServiceImpl(service = SampleNexusService.class) +public class SampleNexusServiceImpl { @OperationImpl - public OperationHandler echo() { + public OperationHandler echo() { // OperationHandler.sync is a meant for exposing simple RPC handlers. return OperationHandler.sync( // The method is for making arbitrary short calls to other services or databases, or @@ -300,11 +300,11 @@ public class NexusServiceImpl { // calling // Nexus.getOperationContext().getWorkflowClient(ctx) to make arbitrary calls such as // signaling, querying, or listing workflows. - (ctx, details, input) -> new NexusService.EchoOutput(input.getMessage())); + (ctx, details, input) -> new SampleNexusService.EchoOutput(input.getMessage())); } @OperationImpl - public OperationHandler hello() { + public OperationHandler hello() { // If the operation input parameters are different from the workflow input parameters, // use the WorkflowRunOperation.fromWorkflowHandler constructor and the appropriate constructor // method on WorkflowHandle to map the Nexus input to the workflow parameters. @@ -362,7 +362,7 @@ public class HandlerWorker { Worker worker = factory.newWorker(DEFAULT_TASK_QUEUE_NAME); worker.registerWorkflowImplementationTypes(HelloHandlerWorkflowImpl.class); - worker.registerNexusServiceImplementation(new NexusServiceImpl()); + worker.registerNexusServiceImplementation(new SampleNexusServiceImpl()); factory.start(); } @@ -379,16 +379,16 @@ Import the Service API package that has the necessary service and operation name ```java package io.temporal.samples.nexus.caller; -import io.temporal.samples.nexus.service.NexusService; +import io.temporal.samples.nexus.service.SampleNexusService; import io.temporal.workflow.NexusOperationOptions; import io.temporal.workflow.NexusServiceOptions; import io.temporal.workflow.Workflow; import java.time.Duration; public class EchoCallerWorkflowImpl implements EchoCallerWorkflow { - NexusService nexusService = + SampleNexusService sampleNexusService = Workflow.newNexusServiceStub( - NexusService.class, + SampleNexusService.class, NexusServiceOptions.newBuilder() .setOperationOptions( NexusOperationOptions.newBuilder() @@ -398,7 +398,7 @@ public class EchoCallerWorkflowImpl implements EchoCallerWorkflow { @Override public String echo(String message) { - return nexusService.echo(new NexusService.EchoInput(message)).getMessage(); + return sampleNexusService.echo(new SampleNexusService.EchoInput(message)).getMessage(); } } ``` @@ -409,7 +409,7 @@ public class EchoCallerWorkflowImpl implements EchoCallerWorkflow { ```java package io.temporal.samples.nexus.caller; -import io.temporal.samples.nexus.service.NexusService; +import io.temporal.samples.nexus.service.SampleNexusService; import io.temporal.workflow.NexusOperationHandle; import io.temporal.workflow.NexusOperationOptions; import io.temporal.workflow.NexusServiceOptions; @@ -417,9 +417,9 @@ import io.temporal.workflow.Workflow; import java.time.Duration; public class HelloCallerWorkflowImpl implements HelloCallerWorkflow { - NexusService nexusService = + SampleNexusService sampleNexusService = Workflow.newNexusServiceStub( - NexusService.class, + SampleNexusService.class, NexusServiceOptions.newBuilder() .setOperationOptions( NexusOperationOptions.newBuilder() @@ -428,10 +428,10 @@ public class HelloCallerWorkflowImpl implements HelloCallerWorkflow { .build()); @Override - public String hello(String message, NexusService.Language language) { - NexusOperationHandle handle = + public String hello(String message, SampleNexusService.Language language) { + NexusOperationHandle handle = Workflow.startNexusOperation( - nexusService::hello, new NexusService.HelloInput(message, language)); + sampleNexusService::hello, new SampleNexusService.HelloInput(message, language)); // Optionally wait for the operation to be started. NexusOperationExecution will contain the // operation token in case this operation is asynchronous. handle.getExecution().get(); @@ -471,7 +471,7 @@ public class CallerWorker { WorkflowImplementationOptions.newBuilder() .setNexusServiceOptions( Collections.singletonMap( - "NexusService", + "SampleNexusService", NexusServiceOptions.newBuilder().setEndpoint("my-nexus-endpoint-name").build())) .build(), EchoCallerWorkflowImpl.class, @@ -496,7 +496,7 @@ import io.temporal.api.common.v1.WorkflowExecution; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; import io.temporal.samples.nexus.options.ClientOptions; -import io.temporal.samples.nexus.service.NexusService; +import io.temporal.samples.nexus.service.SampleNexusService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -518,12 +518,12 @@ public class CallerStarter { logger.info("Workflow result: {}", echoWorkflow.echo("Nexus Echo 👋")); HelloCallerWorkflow helloWorkflow = client.newWorkflowStub(HelloCallerWorkflow.class, workflowOptions); - execution = WorkflowClient.start(helloWorkflow::hello, "Nexus", NexusService.Language.EN); + execution = WorkflowClient.start(helloWorkflow::hello, "Nexus", SampleNexusService.Language.EN); logger.info( "Started HelloCallerWorkflow workflowId: {} runId: {}", execution.getWorkflowId(), execution.getRunId()); - logger.info("Workflow result: {}", helloWorkflow.hello("Nexus", NexusService.Language.ES)); + logger.info("Workflow result: {}", helloWorkflow.hello("Nexus", SampleNexusService.Language.ES)); } } ``` diff --git a/docs/develop/java/testing-suite.mdx b/docs/develop/java/testing-suite.mdx index fb82e3387f..c9095c75c3 100644 --- a/docs/develop/java/testing-suite.mdx +++ b/docs/develop/java/testing-suite.mdx @@ -280,13 +280,229 @@ Activity cancellation lets Activities know they don't need to continue work and Mock the Activity invocation when unit testing your Workflows. When integration testing Workflows with a Worker, you can mock Activities by providing mock Activity implementations to the Worker. +For more details on mocking activities, see [sample unit tests](#sample-unit-tests). ### How to mock Nexus Operations {#mock-nexus-operations} -Mock the Nexus Operation invocation when unit testing your Workflows. +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. -When integration testing Workflows with a Worker, you can mock Nexus Operations by providing mock Nexus Service implementations to the Worker. +You can find example unit tests for Nexus in the [Temporal Java samples](https://github.com/temporalio/samples-java) repository in [this test package](https://github.com/temporalio/samples-java/tree/main/core/src/test/java/io/temporal/samples/nexus/caller). +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, just as in the [sample unit tests](#sample-unit-tests) 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. +Everything is created and set up by the Temporal Testing package, so no more work is needed than that! + +You will need to create workers for each handler just as normal, 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 - again, just as in [the sample unit tests](#sample-unit-tests) above. + +The following are samples derived from [the test package](https://github.com/temporalio/samples-java/tree/main/core/src/test/java/io/temporal/samples/nexus/caller) to demonstrate this. + +#### Mocking Nexus handlers with JUnit4 + +```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 + +```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 simply mock the Nexus service itself, instead of mocking the handlers. +This is useful if you just want to test the calling logic but can't easily mock the Nexus handlers. + +The code will just 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 else they can return an instance variable which each unit test can modify to return a desired value. + +#### Mocking the Nexus Service with JUnit4 + +```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 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 + +```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 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 {#skip-time} Some long-running Workflows can persist for months or even years. From fa3430bb9877cb5820554e5c32934c45082b0937 Mon Sep 17 00:00:00 2001 From: Evan Reynolds Date: Thu, 2 Apr 2026 15:55:01 -0700 Subject: [PATCH 2/2] Adding snippets --- docs/develop/java/testing-suite.mdx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/develop/java/testing-suite.mdx b/docs/develop/java/testing-suite.mdx index c9095c75c3..9a464b2998 100644 --- a/docs/develop/java/testing-suite.mdx +++ b/docs/develop/java/testing-suite.mdx @@ -301,7 +301,8 @@ With that in place, you can then mock a Nexus endpoint exactly like any other wo The following are samples derived from [the test package](https://github.com/temporalio/samples-java/tree/main/core/src/test/java/io/temporal/samples/nexus/caller) to demonstrate this. #### Mocking Nexus handlers with JUnit4 - +{/* SNIPSTART java-nexus-sample-junit4-mock */} +[core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowMockTest.java](https://github.com/temporalio/samples-java/blob/main/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowMockTest.java) ```java public class CallerWorkflowMockTest { @Rule @@ -337,9 +338,11 @@ public class CallerWorkflowMockTest { } } ``` + #### Mocking Nexus handlers with JUnit5 - +{/* SNIPSTART java-nexus-sample-junit5-mock */} +[core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5MockTest.java](https://github.com/temporalio/samples-java/blob/main/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5MockTest.java) ```java public class CallerWorkflowJunit5MockTest { @@ -370,6 +373,7 @@ public class CallerWorkflowJunit5MockTest { } } ``` + An alternative approach is to simply mock the Nexus service itself, instead of mocking the handlers. This is useful if you just want to test the calling logic but can't easily mock the Nexus handlers. @@ -378,7 +382,8 @@ The code will just mock the implementation of the SampleNexusService class with Those methods can be directly mocked with static return values, or else they can return an instance variable which each unit test can modify to return a desired value. #### Mocking the Nexus Service with JUnit4 - +{/* SNIPSTART java-nexus-service-sample-junit4-mock */} +[core/src/test/java/io/temporal/samples/nexus/caller/NexusServiceMockTest.java](https://github.com/temporalio/samples-java/blob/main/core/src/test/java/io/temporal/samples/nexus/caller/NexusServiceMockTest.java) ```java public class NexusServiceMockTest { @@ -437,8 +442,11 @@ public class NexusServiceMockTest { } } ``` -#### Mocking the Nexus Service with JUnit5 + +#### Mocking the Nexus Service with JUnit5 +{/* SNIPSTART java-nexus-service-sample-junit5-mock */} +[core/src/test/java/io/temporal/samples/nexus/caller/NexusServiceJunit5Test.java](https://github.com/temporalio/samples-java/blob/main/core/src/test/java/io/temporal/samples/nexus/caller/NexusServiceJunit5Test.java) ```java public class NexusServiceJunit5Test { @@ -503,6 +511,8 @@ public class NexusServiceJunit5Test { } } ``` + + ### How to skip time {#skip-time} Some long-running Workflows can persist for months or even years.