diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/EchoHandler.java b/core/src/main/java/io/temporal/samples/nexus/handler/EchoHandler.java new file mode 100644 index 000000000..eee147cc5 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexus/handler/EchoHandler.java @@ -0,0 +1,7 @@ +package io.temporal.samples.nexus.handler; + +import io.temporal.samples.nexus.service.NexusService; + +public interface EchoHandler { + NexusService.EchoOutput echo(NexusService.EchoInput input); +} diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/EchoHandlerImpl.java b/core/src/main/java/io/temporal/samples/nexus/handler/EchoHandlerImpl.java new file mode 100644 index 000000000..191309b09 --- /dev/null +++ b/core/src/main/java/io/temporal/samples/nexus/handler/EchoHandlerImpl.java @@ -0,0 +1,12 @@ +package io.temporal.samples.nexus.handler; + +import io.temporal.samples.nexus.service.NexusService; + +// Note that this is a class, not a Temporal worker. This is to demonstrate that Nexus services can +// simply call a class instead of a worker for fast operations that don't need retry handling. +public class EchoHandlerImpl implements EchoHandler { + @Override + public NexusService.EchoOutput echo(NexusService.EchoInput input) { + return new NexusService.EchoOutput(input.getMessage()); + } +} diff --git a/core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java b/core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java index 2344f27ec..8bb17d215 100644 --- a/core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java +++ b/core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java @@ -13,16 +13,32 @@ // return OperationHandler that correspond to the operations defined in the service interface. @ServiceImpl(service = NexusService.class) public class NexusServiceImpl { + private final EchoHandler echoHandler; + + // The injected EchoHandler makes this class unit-testable. + // The no-arg constructor provides a default; the second allows tests to inject a mock. + // If you are not using the sync call or do not need to mock a handler, then you will not + // need this constructor pairing. + public NexusServiceImpl() { + this(new EchoHandlerImpl()); + } + + public NexusServiceImpl(EchoHandler echoHandler) { + this.echoHandler = echoHandler; + } + + // The Echo Nexus Service exemplifies making a synchronous call using OperationHandler.sync. + // In this case, it is calling the EchoHandler class - not a workflow - and simply returning the + // result. @OperationImpl 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 // perform simple computations such as this one. Users can also access a workflow client by // 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) -> echoHandler.echo(input)); } @OperationImpl @@ -39,10 +55,8 @@ public OperationHandler hello .newWorkflowStub( HelloHandlerWorkflow.class, // Workflow IDs should typically be business meaningful IDs and are used to - // dedupe workflow starts. - // For this example, we're using the request ID allocated by Temporal when - // the - // caller workflow schedules + // dedupe workflow starts. For this example, we're using the request ID + // allocated by Temporal when the caller workflow schedules // the operation, this ID is guaranteed to be stable across retries of this // operation. // diff --git a/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5MockTest.java b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5MockTest.java new file mode 100644 index 000000000..f19a4fe65 --- /dev/null +++ b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5MockTest.java @@ -0,0 +1,76 @@ +package io.temporal.samples.nexus.caller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +import io.temporal.samples.nexus.handler.EchoHandler; +import io.temporal.samples.nexus.handler.HelloHandlerWorkflow; +import io.temporal.samples.nexus.handler.NexusServiceImpl; +import io.temporal.samples.nexus.service.NexusService; +import io.temporal.testing.TestWorkflowEnvironment; +import io.temporal.testing.TestWorkflowExtension; +import io.temporal.worker.Worker; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class CallerWorkflowJunit5MockTest { + + // Sync Nexus operations run inline in the handler thread — there is no backing workflow to + // register a factory for. To mock one, inject a mock dependency into the service implementation. + private static final EchoHandler mockEchoHandler = mock(EchoHandler.class); + + @RegisterExtension + public static 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 NexusServiceImpl(mockEchoHandler)) + // The Echo Nexus handler service just makes a call to a class, so no extra setup is + // needed. But the Hello Nexus service needs a worker for both the caller and handler + // in order to run, and the Echo Nexus caller service needs a worker. + // + // registerWorkflowImplementationTypes will take the classes given and create workers for + // them, enabling workflows to run. + .registerWorkflowImplementationTypes( + HelloCallerWorkflowImpl.class, EchoCallerWorkflowImpl.class) + .setDoNotStart(true) + .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 NexusService.HelloOutput("Hello Mock World 👋")); + return mockHandler; + }); + testEnv.start(); + + // Execute a workflow waiting for it to complete. + String greeting = workflow.hello("World", NexusService.Language.EN); + assertEquals("Hello Mock World 👋", greeting); + + testEnv.shutdown(); + } + + @Test + public void testEchoWorkflow( + TestWorkflowEnvironment testEnv, Worker worker, EchoCallerWorkflow workflow) { + // Sync Nexus operations run inline in the handler thread — there is no backing workflow to + // register a factory for. Instead, stub the injected EchoHandler dependency directly. + when(mockEchoHandler.echo(any())).thenReturn(new NexusService.EchoOutput("mocked echo")); + testEnv.start(); + + // Execute a workflow waiting for it to complete. + String greeting = workflow.echo("Hello"); + assertEquals("mocked echo", greeting); + + testEnv.shutdown(); + } +} diff --git a/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5Test.java b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5Test.java new file mode 100644 index 000000000..c7e32335a --- /dev/null +++ b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowJunit5Test.java @@ -0,0 +1,55 @@ +package io.temporal.samples.nexus.caller; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.temporal.samples.nexus.handler.HelloHandlerWorkflowImpl; +import io.temporal.samples.nexus.handler.NexusServiceImpl; +import io.temporal.samples.nexus.service.NexusService; +import io.temporal.testing.TestWorkflowEnvironment; +import io.temporal.testing.TestWorkflowExtension; +import io.temporal.worker.Worker; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class CallerWorkflowJunit5Test { + + @RegisterExtension + public static 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 NexusServiceImpl()) + // The Echo Nexus handler service just makes a call to a class, so no extra setup is + // needed. But the Hello Nexus service needs a worker for both the caller and handler + // in order to run, and the Echo Nexus caller service needs a worker. + // + // registerWorkflowImplementationTypes will take the classes given and create workers for + // them, enabling workflows to run. + .registerWorkflowImplementationTypes( + HelloCallerWorkflowImpl.class, + HelloHandlerWorkflowImpl.class, + EchoCallerWorkflowImpl.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) { + // Execute a workflow waiting for it to complete. + String greeting = workflow.hello("World", NexusService.Language.EN); + assertEquals("Hello World 👋", greeting); + } + + @Test + public void testEchoWorkflow( + TestWorkflowEnvironment testEnv, Worker worker, EchoCallerWorkflow workflow) { + // Execute a workflow waiting for it to complete. + String greeting = workflow.echo("Hello"); + assertEquals("Hello", greeting); + } +} diff --git a/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowMockTest.java b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowMockTest.java new file mode 100644 index 000000000..405b48f77 --- /dev/null +++ b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowMockTest.java @@ -0,0 +1,88 @@ +package io.temporal.samples.nexus.caller; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.temporal.client.WorkflowOptions; +import io.temporal.samples.nexus.handler.EchoHandler; +import io.temporal.samples.nexus.handler.HelloHandlerWorkflow; +import io.temporal.samples.nexus.handler.NexusServiceImpl; +import io.temporal.samples.nexus.service.NexusService; +import io.temporal.testing.TestWorkflowRule; +import org.junit.Rule; +import org.junit.Test; + +public class CallerWorkflowMockTest { + + // Inject a mock EchoHandler so sync Nexus operations can be stubbed per test. + // JUnit 4 creates a new test class instance per test method, so this mock is fresh each time. + private final EchoHandler mockEchoHandler = mock(EchoHandler.class); + + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + // If a Nexus service is registered as part of the test as in the following line of code, + // the TestWorkflowRule will, by default, automatically create a Nexus service endpoint + // and workflows registered as part of the TestWorkflowRule + // will automatically inherit the endpoint if none is set. + .setNexusServiceImplementation(new NexusServiceImpl(mockEchoHandler)) + // The Echo Nexus handler service just makes a call to a class, so no extra setup is + // needed. But the Hello Nexus service needs a worker for both the caller and handler + // in order to run. + // setWorkflowTypes will take the classes given and create workers for them, enabling + // workflows to run. This creates caller workflows, the handler workflows + // will be mocked in the test methods. + .setWorkflowTypes(HelloCallerWorkflowImpl.class, EchoCallerWorkflowImpl.class) + // Disable automatic worker startup as we are going to register some workflows manually + // per test + .setDoNotStart(true) + .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 NexusService.HelloOutput("Hello Mock World 👋")); + return wf; + }); + testWorkflowRule.getTestEnvironment().start(); + + // Now create the caller workflow + HelloCallerWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + HelloCallerWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + String greeting = workflow.hello("World", NexusService.Language.EN); + assertEquals("Hello Mock World 👋", greeting); + + testWorkflowRule.getTestEnvironment().shutdown(); + } + + @Test + public void testEchoWorkflow() { + // Sync Nexus operations run inline in the handler thread — there is no backing workflow to + // register a factory for. Instead, stub the injected EchoHandler dependency directly. + when(mockEchoHandler.echo(any())).thenReturn(new NexusService.EchoOutput("mocked echo")); + testWorkflowRule.getTestEnvironment().start(); + + EchoCallerWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + EchoCallerWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + String greeting = workflow.echo("Hello"); + assertEquals("mocked echo", greeting); + + testWorkflowRule.getTestEnvironment().shutdown(); + } +} diff --git a/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowTest.java b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowTest.java index 2d3a6e42a..683aff32f 100644 --- a/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowTest.java +++ b/core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowTest.java @@ -1,12 +1,9 @@ package io.temporal.samples.nexus.caller; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import io.temporal.client.WorkflowOptions; -import io.temporal.samples.nexus.handler.HelloHandlerWorkflow; +import io.temporal.samples.nexus.handler.HelloHandlerWorkflowImpl; import io.temporal.samples.nexus.handler.NexusServiceImpl; import io.temporal.samples.nexus.service.NexusService; import io.temporal.testing.TestWorkflowRule; @@ -21,11 +18,19 @@ public class CallerWorkflowTest { @Rule public TestWorkflowRule testWorkflowRule = TestWorkflowRule.newBuilder() - // If a Nexus service is registered as part of the test, the TestWorkflowRule will ,by - // default, automatically create a Nexus service endpoint and workflows registered as part - // of the TestWorkflowRule will automatically inherit the endpoint if none is set. + // If a Nexus service is registered as part of the test as in the following line of code, + // the TestWorkflowRule will, by default, automatically create a Nexus service endpoint + // and workflows registered as part of the TestWorkflowRule + // will automatically inherit the endpoint if none is set. .setNexusServiceImplementation(new NexusServiceImpl()) - .setWorkflowTypes(HelloCallerWorkflowImpl.class) + // The Echo Nexus handler service just makes a call to a class, so no extra setup is + // needed. But the Hello Nexus service needs a worker for both the caller and handler + // in order to run. + // setWorkflowTypes will take the classes given and create workers for them, enabling + // workflows to run. This is not adding an EchoCallerWorkflow though - + // see the testEchoWorkflow test method below for an example of an alternate way + // to supply a worker that gives you more flexibility if needed. + .setWorkflowTypes(HelloCallerWorkflowImpl.class, HelloHandlerWorkflowImpl.class) // Disable automatic worker startup as we are going to register some workflows manually // per test .setDoNotStart(true) @@ -33,16 +38,6 @@ public class CallerWorkflowTest { @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 NexusService.HelloOutput("Hello World 👋")); - return wf; - }); testWorkflowRule.getTestEnvironment().start(); HelloCallerWorkflow workflow = @@ -61,8 +56,13 @@ public void testHelloWorkflow() { public void testEchoWorkflow() { // If Workflows are registered later than the endpoint can be set manually // either by setting the endpoint in the NexusServiceOptions in the Workflow implementation or - // by setting the NexusServiceOptions on the WorkflowImplementationOptions when registering the - // Workflow. + // by setting the NexusServiceOptions on the WorkflowImplementationOptions when registering + // the Workflow. To demonstrate, this is creating the Nexus service for Echo, + // and registering a EchoCallerWorkflowImpl worker. + // + // It is much simpler to use the setWorkflowTypes in the rule definition above - and as + // this isn't easily do-able in JUnit5 (the nexus endpoint isn't exposed) should be + // used with caution. testWorkflowRule .getWorker() .registerWorkflowImplementationTypes(