Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<NexusService.EchoInput, NexusService.EchoOutput> 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
Expand All @@ -39,10 +55,8 @@ public OperationHandler<NexusService.HelloInput, NexusService.HelloOutput> 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.
//
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,28 +18,26 @@ 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)
.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 World 👋"));
return wf;
});
testWorkflowRule.getTestEnvironment().start();

HelloCallerWorkflow workflow =
Expand All @@ -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(
Expand Down
Loading