Skip to content

Commit 47da04e

Browse files
committed
Code samples for testing and mocking Nexus
1 parent bf3515d commit 47da04e

File tree

7 files changed

+274
-26
lines changed

7 files changed

+274
-26
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.temporal.samples.nexus.handler;
2+
3+
import io.temporal.samples.nexus.service.NexusService;
4+
5+
public interface EchoHandler {
6+
NexusService.EchoOutput echo(NexusService.EchoInput input);
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.temporal.samples.nexus.handler;
2+
3+
import io.temporal.samples.nexus.service.NexusService;
4+
5+
// Note that this is a class, not a Temporal worker. This is to demonstrate that Nexus services can
6+
// simply call a class instead of a worker for fast operations that don't need retry handling.
7+
public class EchoHandlerImpl implements EchoHandler {
8+
@Override
9+
public NexusService.EchoOutput echo(NexusService.EchoInput input) {
10+
return new NexusService.EchoOutput(input.getMessage());
11+
}
12+
}

core/src/main/java/io/temporal/samples/nexus/handler/NexusServiceImpl.java

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,32 @@
1313
// return OperationHandler that correspond to the operations defined in the service interface.
1414
@ServiceImpl(service = NexusService.class)
1515
public class NexusServiceImpl {
16+
private final EchoHandler echoHandler;
17+
18+
// The injected EchoHandler makes this class unit-testable.
19+
// The no-arg constructor provides a default; the second allows tests to inject a mock.
20+
// If you are not using the sync call or do not need to mock a handler, then you will not
21+
// need this constructor pairing.
22+
public NexusServiceImpl() {
23+
this(new EchoHandlerImpl());
24+
}
25+
26+
public NexusServiceImpl(EchoHandler echoHandler) {
27+
this.echoHandler = echoHandler;
28+
}
29+
30+
// The Echo Nexus Service exemplifies making a synchronous call using OperationHandler.sync.
31+
// In this case, it is calling the EchoHandler class - not a workflow - and simply returning the
32+
// result.
1633
@OperationImpl
1734
public OperationHandler<NexusService.EchoInput, NexusService.EchoOutput> echo() {
18-
// OperationHandler.sync is a meant for exposing simple RPC handlers.
1935
return OperationHandler.sync(
2036
// The method is for making arbitrary short calls to other services or databases, or
2137
// perform simple computations such as this one. Users can also access a workflow client by
2238
// calling
2339
// Nexus.getOperationContext().getWorkflowClient(ctx) to make arbitrary calls such as
2440
// signaling, querying, or listing workflows.
25-
(ctx, details, input) -> new NexusService.EchoOutput(input.getMessage()));
41+
(ctx, details, input) -> echoHandler.echo(input));
2642
}
2743

2844
@OperationImpl
@@ -39,10 +55,8 @@ public OperationHandler<NexusService.HelloInput, NexusService.HelloOutput> hello
3955
.newWorkflowStub(
4056
HelloHandlerWorkflow.class,
4157
// Workflow IDs should typically be business meaningful IDs and are used to
42-
// dedupe workflow starts.
43-
// For this example, we're using the request ID allocated by Temporal when
44-
// the
45-
// caller workflow schedules
58+
// dedupe workflow starts. For this example, we're using the request ID
59+
// allocated by Temporal when the caller workflow schedules
4660
// the operation, this ID is guaranteed to be stable across retries of this
4761
// operation.
4862
//
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package io.temporal.samples.nexus.caller;
2+
3+
import io.temporal.samples.nexus.handler.EchoHandler;
4+
import io.temporal.samples.nexus.handler.HelloHandlerWorkflow;
5+
import io.temporal.samples.nexus.handler.NexusServiceImpl;
6+
import io.temporal.samples.nexus.service.NexusService;
7+
import io.temporal.testing.TestWorkflowEnvironment;
8+
import io.temporal.testing.TestWorkflowExtension;
9+
import io.temporal.worker.Worker;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.mockito.Mockito.*;
15+
16+
public class CallerWorkflowJunit5MockTest {
17+
18+
// Sync Nexus operations run inline in the handler thread — there is no backing workflow to
19+
// register a factory for. To mock one, inject a mock dependency into the service implementation.
20+
private static final EchoHandler mockEchoHandler = mock(EchoHandler.class);
21+
22+
@RegisterExtension
23+
public static final TestWorkflowExtension testWorkflowExtension =
24+
TestWorkflowExtension.newBuilder()
25+
// If a Nexus service is registered as part of the test as in the following line of code,
26+
// the TestWorkflowExtension will, by default, automatically create a Nexus service endpoint
27+
// and workflows registered as part of the TestWorkflowExtension will
28+
// automatically inherit the endpoint if none is set.
29+
.setNexusServiceImplementation(new NexusServiceImpl(mockEchoHandler))
30+
// The Echo Nexus handler service just makes a call to a class, so no extra setup is
31+
// needed. But the Hello Nexus service needs a worker for both the caller and handler
32+
// in order to run, and the Echo Nexus caller service needs a worker.
33+
//
34+
// registerWorkflowImplementationTypes will take the classes given and create workers for them,
35+
// enabling workflows to run.
36+
.registerWorkflowImplementationTypes(
37+
HelloCallerWorkflowImpl.class, EchoCallerWorkflowImpl.class)
38+
.setDoNotStart(true)
39+
.build();
40+
41+
@Test
42+
public void testHelloWorkflow(TestWorkflowEnvironment testEnv, Worker worker, HelloCallerWorkflow workflow) {
43+
// Workflows started by a Nexus service can be mocked just like any other workflow
44+
worker.registerWorkflowImplementationFactory(
45+
HelloHandlerWorkflow.class,
46+
() -> {
47+
HelloHandlerWorkflow mockHandler = mock(HelloHandlerWorkflow.class);
48+
when(mockHandler.hello(any()))
49+
.thenReturn(new NexusService.HelloOutput("Hello Mock World 👋"));
50+
return mockHandler;
51+
});
52+
testEnv.start();
53+
54+
// Execute a workflow waiting for it to complete.
55+
String greeting = workflow.hello("World", NexusService.Language.EN);
56+
assertEquals("Hello Mock World 👋", greeting);
57+
58+
testEnv.shutdown();
59+
}
60+
61+
@Test
62+
public void testEchoWorkflow(TestWorkflowEnvironment testEnv, Worker worker, EchoCallerWorkflow workflow) {
63+
// Sync Nexus operations run inline in the handler thread — there is no backing workflow to
64+
// register a factory for. Instead, stub the injected EchoHandler dependency directly.
65+
when(mockEchoHandler.echo(any())).thenReturn(new NexusService.EchoOutput("mocked echo"));
66+
testEnv.start();
67+
68+
// Execute a workflow waiting for it to complete.
69+
String greeting = workflow.echo("Hello");
70+
assertEquals("mocked echo", greeting);
71+
72+
testEnv.shutdown();
73+
}
74+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.temporal.samples.nexus.caller;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import io.temporal.samples.nexus.handler.HelloHandlerWorkflowImpl;
6+
import io.temporal.samples.nexus.handler.NexusServiceImpl;
7+
import io.temporal.samples.nexus.service.NexusService;
8+
import io.temporal.testing.TestWorkflowEnvironment;
9+
import io.temporal.testing.TestWorkflowExtension;
10+
import io.temporal.worker.Worker;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.RegisterExtension;
13+
14+
public class CallerWorkflowJunit5Test {
15+
16+
@RegisterExtension
17+
public static final TestWorkflowExtension testWorkflowExtension =
18+
TestWorkflowExtension.newBuilder()
19+
// If a Nexus service is registered as part of the test as in the following line of code,
20+
// the TestWorkflowExtension will, by default, automatically create a Nexus service endpoint
21+
// and workflows registered as part of the TestWorkflowExtension will
22+
// automatically inherit the endpoint if none is set.
23+
.setNexusServiceImplementation(new NexusServiceImpl())
24+
// The Echo Nexus handler service just makes a call to a class, so no extra setup is
25+
// needed. But the Hello Nexus service needs a worker for both the caller and handler
26+
// in order to run, and the Echo Nexus caller service needs a worker.
27+
//
28+
// registerWorkflowImplementationTypes will take the classes given and create workers for them,
29+
// enabling workflows to run.
30+
.registerWorkflowImplementationTypes(
31+
HelloCallerWorkflowImpl.class,
32+
HelloHandlerWorkflowImpl.class,
33+
EchoCallerWorkflowImpl.class)
34+
// The workflow will start before each test, and will shut down after each test.
35+
// See CallerWorkflowTest for an example of how to control this differently if needed.
36+
.build();
37+
38+
// The TestWorkflowExtension extension in the Temporal testing library creates the
39+
// arguments to the test cases and initializes them from the extension setup call above.
40+
@Test
41+
public void testHelloWorkflow(TestWorkflowEnvironment testEnv, Worker worker, HelloCallerWorkflow workflow) {
42+
// Execute a workflow waiting for it to complete.
43+
String greeting = workflow.hello("World", NexusService.Language.EN);
44+
assertEquals("Hello World 👋", greeting);
45+
}
46+
47+
@Test
48+
public void testEchoWorkflow(TestWorkflowEnvironment testEnv, Worker worker, EchoCallerWorkflow workflow) {
49+
// Execute a workflow waiting for it to complete.
50+
String greeting = workflow.echo("Hello");
51+
assertEquals("Hello", greeting);
52+
}
53+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.temporal.samples.nexus.caller;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.when;
7+
8+
import io.temporal.client.WorkflowOptions;
9+
import io.temporal.samples.nexus.handler.EchoHandler;
10+
import io.temporal.samples.nexus.handler.HelloHandlerWorkflow;
11+
import io.temporal.samples.nexus.handler.NexusServiceImpl;
12+
import io.temporal.samples.nexus.service.NexusService;
13+
import io.temporal.testing.TestWorkflowRule;
14+
import org.junit.Rule;
15+
import org.junit.Test;
16+
17+
public class CallerWorkflowMockTest {
18+
19+
// Inject a mock EchoHandler so sync Nexus operations can be stubbed per test.
20+
// JUnit 4 creates a new test class instance per test method, so this mock is fresh each time.
21+
private final EchoHandler mockEchoHandler = mock(EchoHandler.class);
22+
23+
@Rule
24+
public TestWorkflowRule testWorkflowRule =
25+
TestWorkflowRule.newBuilder()
26+
// If a Nexus service is registered as part of the test as in the following line of code,
27+
// the TestWorkflowRule will, by default, automatically create a Nexus service endpoint
28+
// and workflows registered as part of the TestWorkflowRule
29+
// will automatically inherit the endpoint if none is set.
30+
.setNexusServiceImplementation(new NexusServiceImpl(mockEchoHandler))
31+
// The Echo Nexus handler service just makes a call to a class, so no extra setup is
32+
// needed. But the Hello Nexus service needs a worker for both the caller and handler
33+
// in order to run.
34+
// setWorkflowTypes will take the classes given and create workers for them, enabling
35+
// workflows to run. This creates caller workflows, the handler workflows
36+
// will be mocked in the test methods.
37+
.setWorkflowTypes(HelloCallerWorkflowImpl.class, EchoCallerWorkflowImpl.class)
38+
// Disable automatic worker startup as we are going to register some workflows manually
39+
// per test
40+
.setDoNotStart(true)
41+
.build();
42+
43+
@Test
44+
public void testHelloWorkflow() {
45+
testWorkflowRule
46+
.getWorker()
47+
// Workflows started by a Nexus service can be mocked just like any other workflow
48+
.registerWorkflowImplementationFactory(
49+
HelloHandlerWorkflow.class,
50+
() -> {
51+
HelloHandlerWorkflow wf = mock(HelloHandlerWorkflow.class);
52+
when(wf.hello(any())).thenReturn(new NexusService.HelloOutput("Hello Mock World 👋"));
53+
return wf;
54+
});
55+
testWorkflowRule.getTestEnvironment().start();
56+
57+
// Now create the caller workflow
58+
HelloCallerWorkflow workflow =
59+
testWorkflowRule
60+
.getWorkflowClient()
61+
.newWorkflowStub(
62+
HelloCallerWorkflow.class,
63+
WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build());
64+
String greeting = workflow.hello("World", NexusService.Language.EN);
65+
assertEquals("Hello Mock World 👋", greeting);
66+
67+
testWorkflowRule.getTestEnvironment().shutdown();
68+
}
69+
70+
@Test
71+
public void testEchoWorkflow() {
72+
// Sync Nexus operations run inline in the handler thread — there is no backing workflow to
73+
// register a factory for. Instead, stub the injected EchoHandler dependency directly.
74+
when(mockEchoHandler.echo(any())).thenReturn(new NexusService.EchoOutput("mocked echo"));
75+
testWorkflowRule.getTestEnvironment().start();
76+
77+
EchoCallerWorkflow workflow =
78+
testWorkflowRule
79+
.getWorkflowClient()
80+
.newWorkflowStub(
81+
EchoCallerWorkflow.class,
82+
WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build());
83+
String greeting = workflow.echo("Hello");
84+
assertEquals("mocked echo", greeting);
85+
86+
testWorkflowRule.getTestEnvironment().shutdown();
87+
}
88+
}

core/src/test/java/io/temporal/samples/nexus/caller/CallerWorkflowTest.java

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package io.temporal.samples.nexus.caller;
22

33
import static org.junit.Assert.assertEquals;
4-
import static org.mockito.ArgumentMatchers.any;
5-
import static org.mockito.Mockito.mock;
6-
import static org.mockito.Mockito.when;
74

85
import io.temporal.client.WorkflowOptions;
9-
import io.temporal.samples.nexus.handler.HelloHandlerWorkflow;
6+
import io.temporal.samples.nexus.handler.HelloHandlerWorkflowImpl;
107
import io.temporal.samples.nexus.handler.NexusServiceImpl;
118
import io.temporal.samples.nexus.service.NexusService;
129
import io.temporal.testing.TestWorkflowRule;
@@ -21,28 +18,26 @@ public class CallerWorkflowTest {
2118
@Rule
2219
public TestWorkflowRule testWorkflowRule =
2320
TestWorkflowRule.newBuilder()
24-
// If a Nexus service is registered as part of the test, the TestWorkflowRule will ,by
25-
// default, automatically create a Nexus service endpoint and workflows registered as part
26-
// of the TestWorkflowRule will automatically inherit the endpoint if none is set.
21+
// If a Nexus service is registered as part of the test as in the following line of code,
22+
// the TestWorkflowRule will, by default, automatically create a Nexus service endpoint
23+
// and workflows registered as part of the TestWorkflowRule
24+
// will automatically inherit the endpoint if none is set.
2725
.setNexusServiceImplementation(new NexusServiceImpl())
28-
.setWorkflowTypes(HelloCallerWorkflowImpl.class)
26+
// The Echo Nexus handler service just makes a call to a class, so no extra setup is
27+
// needed. But the Hello Nexus service needs a worker for both the caller and handler
28+
// in order to run.
29+
// setWorkflowTypes will take the classes given and create workers for them, enabling
30+
// workflows to run. This is not adding an EchoCallerWorkflow though -
31+
// see the testEchoWorkflow test method below for an example of an alternate way
32+
// to supply a worker that gives you more flexibility if needed.
33+
.setWorkflowTypes(HelloCallerWorkflowImpl.class, HelloHandlerWorkflowImpl.class)
2934
// Disable automatic worker startup as we are going to register some workflows manually
3035
// per test
3136
.setDoNotStart(true)
3237
.build();
3338

3439
@Test
3540
public void testHelloWorkflow() {
36-
testWorkflowRule
37-
.getWorker()
38-
// Workflows started by a Nexus service can be mocked just like any other workflow
39-
.registerWorkflowImplementationFactory(
40-
HelloHandlerWorkflow.class,
41-
() -> {
42-
HelloHandlerWorkflow wf = mock(HelloHandlerWorkflow.class);
43-
when(wf.hello(any())).thenReturn(new NexusService.HelloOutput("Hello World 👋"));
44-
return wf;
45-
});
4641
testWorkflowRule.getTestEnvironment().start();
4742

4843
HelloCallerWorkflow workflow =
@@ -61,8 +56,13 @@ public void testHelloWorkflow() {
6156
public void testEchoWorkflow() {
6257
// If Workflows are registered later than the endpoint can be set manually
6358
// either by setting the endpoint in the NexusServiceOptions in the Workflow implementation or
64-
// by setting the NexusServiceOptions on the WorkflowImplementationOptions when registering the
65-
// Workflow.
59+
// by setting the NexusServiceOptions on the WorkflowImplementationOptions when registering
60+
// the Workflow. To demonstrate, this is creating the Nexus service for Echo,
61+
// and registering a EchoCallerWorkflowImpl worker.
62+
//
63+
// It is much simpler to use the setWorkflowTypes in the rule definition above - and as
64+
// this isn't easily do-able in JUnit5 (the nexus endpoint isn't exposed) should be
65+
// used with caution.
6666
testWorkflowRule
6767
.getWorker()
6868
.registerWorkflowImplementationTypes(

0 commit comments

Comments
 (0)