Skip to content

Commit 99e7103

Browse files
authored
Polish JUnit5 extension: per-test initial time, dynamic workflows and activities (#581)
Override per-test initial workflow time via the @WorkflowInitialTime annotation. Extend TestActivityExtension and TestWorkflowExtension to support dynamic activity and workflow implementations. If a dynamic implementation is registered with the extension, it would support injecting any typed stub into a test.
1 parent d525026 commit 99e7103

7 files changed

Lines changed: 286 additions & 25 deletions

File tree

temporal-testing-junit5/src/main/java/io/temporal/testing/TestActivityExtension.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919

2020
package io.temporal.testing;
2121

22+
import io.temporal.activity.DynamicActivity;
2223
import io.temporal.common.metadata.POJOActivityImplMetadata;
2324
import io.temporal.common.metadata.POJOActivityInterfaceMetadata;
2425
import java.lang.reflect.Constructor;
26+
import java.lang.reflect.Parameter;
2527
import java.util.HashSet;
2628
import java.util.Set;
2729
import org.junit.jupiter.api.extension.AfterEachCallback;
@@ -67,13 +69,19 @@ public class TestActivityExtension
6769

6870
private final Set<Class<?>> supportedParameterTypes = new HashSet<>();
6971

72+
private boolean includesDynamicActivity;
73+
7074
private TestActivityExtension(Builder builder) {
7175
testEnvironmentOptions = builder.testEnvironmentOptions;
7276
activityImplementations = builder.activityImplementations;
7377

7478
supportedParameterTypes.add(TestActivityEnvironment.class);
7579

7680
for (Object activity : activityImplementations) {
81+
if (DynamicActivity.class.isAssignableFrom(activity.getClass())) {
82+
includesDynamicActivity = true;
83+
continue;
84+
}
7785
POJOActivityImplMetadata metadata = POJOActivityImplMetadata.newInstance(activity.getClass());
7886
for (POJOActivityInterfaceMetadata activityInterface : metadata.getActivityInterfaces()) {
7987
supportedParameterTypes.add(activityInterface.getInterfaceClass());
@@ -90,13 +98,31 @@ public boolean supportsParameter(
9098
ParameterContext parameterContext, ExtensionContext extensionContext)
9199
throws ParameterResolutionException {
92100

93-
if (parameterContext.getParameter().getDeclaringExecutable() instanceof Constructor) {
101+
Parameter parameter = parameterContext.getParameter();
102+
if (parameter.getDeclaringExecutable() instanceof Constructor) {
94103
// Constructor injection is not supported
95104
return false;
96105
}
97106

98-
Class<?> parameterType = parameterContext.getParameter().getType();
99-
return supportedParameterTypes.contains(parameterType);
107+
Class<?> parameterType = parameter.getType();
108+
if (supportedParameterTypes.contains(parameterType)) {
109+
return true;
110+
}
111+
112+
if (!includesDynamicActivity) {
113+
// If no DynamicActivity implementation was registered then supportedParameterTypes are the
114+
// only ones types that can be injected
115+
return false;
116+
}
117+
118+
try {
119+
// If POJOActivityInterfaceMetadata can be instantiated then parameterType is a proper
120+
// activity interface and can be injected
121+
POJOActivityInterfaceMetadata.newInstance(parameterType);
122+
return true;
123+
} catch (Exception e) {
124+
return false;
125+
}
100126
}
101127

102128
@Override

temporal-testing-junit5/src/main/java/io/temporal/testing/TestWorkflowExtension.java

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
import io.temporal.worker.Worker;
2929
import io.temporal.worker.WorkerFactoryOptions;
3030
import io.temporal.worker.WorkerOptions;
31+
import io.temporal.workflow.DynamicWorkflow;
3132
import java.lang.reflect.Constructor;
33+
import java.lang.reflect.Parameter;
3234
import java.time.Instant;
3335
import java.util.HashSet;
3436
import java.util.Set;
@@ -40,6 +42,7 @@
4042
import org.junit.jupiter.api.extension.ParameterResolutionException;
4143
import org.junit.jupiter.api.extension.ParameterResolver;
4244
import org.junit.jupiter.api.extension.TestWatcher;
45+
import org.junit.platform.commons.support.AnnotationSupport;
4346

4447
/**
4548
* JUnit Jupiter extension that simplifies testing of Temporal workflows.
@@ -77,7 +80,7 @@ public class TestWorkflowExtension
7780

7881
private static final String TEST_ENVIRONMENT_KEY = "testEnvironment";
7982
private static final String WORKER_KEY = "worker";
80-
private static final String TASK_QUEUE_KEY = "taskQueue";
83+
private static final String WORKFLOW_OPTIONS_KEY = "workflowOptions";
8184

8285
private final WorkerOptions workerOptions;
8386
private final WorkflowClientOptions workflowClientOptions;
@@ -90,6 +93,7 @@ public class TestWorkflowExtension
9093
private final long initialTimeMillis;
9194

9295
private final Set<Class<?>> supportedParameterTypes = new HashSet<>();
96+
private boolean includesDynamicWorkflow;
9397

9498
private TestWorkflowExtension(Builder builder) {
9599
workerOptions = builder.workerOptions;
@@ -113,6 +117,10 @@ private TestWorkflowExtension(Builder builder) {
113117
supportedParameterTypes.add(Worker.class);
114118

115119
for (Class<?> workflowType : workflowTypes) {
120+
if (DynamicWorkflow.class.isAssignableFrom(workflowType)) {
121+
includesDynamicWorkflow = true;
122+
continue;
123+
}
116124
POJOWorkflowImplMetadata metadata = POJOWorkflowImplMetadata.newInstance(workflowType);
117125
for (POJOWorkflowInterfaceMetadata workflowInterface : metadata.getWorkflowInterfaces()) {
118126
supportedParameterTypes.add(workflowInterface.getInterfaceClass());
@@ -129,50 +137,68 @@ public boolean supportsParameter(
129137
ParameterContext parameterContext, ExtensionContext extensionContext)
130138
throws ParameterResolutionException {
131139

132-
if (parameterContext.getParameter().getDeclaringExecutable() instanceof Constructor) {
140+
Parameter parameter = parameterContext.getParameter();
141+
if (parameter.getDeclaringExecutable() instanceof Constructor) {
133142
// Constructor injection is not supported
134143
return false;
135144
}
136145

137-
Class<?> parameterType = parameterContext.getParameter().getType();
138-
return supportedParameterTypes.contains(parameterType);
146+
Class<?> parameterType = parameter.getType();
147+
if (supportedParameterTypes.contains(parameterType)) {
148+
return true;
149+
}
150+
151+
if (!includesDynamicWorkflow) {
152+
// If no DynamicWorkflow implementation was registered then supportedParameterTypes are the
153+
// only ones types that can be injected
154+
return false;
155+
}
156+
157+
try {
158+
// If POJOWorkflowInterfaceMetadata can be instantiated then parameterType is a proper
159+
// workflow interface and can be injected
160+
POJOWorkflowInterfaceMetadata.newInstance(parameterType);
161+
return true;
162+
} catch (Exception e) {
163+
return false;
164+
}
139165
}
140166

141167
@Override
142168
public Object resolveParameter(
143169
ParameterContext parameterContext, ExtensionContext extensionContext)
144170
throws ParameterResolutionException {
145171

146-
TestWorkflowEnvironment testEnvironment = getTestEnvironment(extensionContext);
147-
148172
Class<?> parameterType = parameterContext.getParameter().getType();
149173
if (parameterType == TestWorkflowEnvironment.class) {
150-
return testEnvironment;
174+
return getTestEnvironment(extensionContext);
151175
} else if (parameterType == WorkflowClient.class) {
152-
return testEnvironment.getWorkflowClient();
176+
return getTestEnvironment(extensionContext).getWorkflowClient();
153177
} else if (parameterType == WorkflowOptions.class) {
154-
String taskQueue = getTaskQueue(extensionContext);
155-
return WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build();
178+
return getWorkflowOptions(extensionContext);
156179
} else if (parameterType == Worker.class) {
157180
return getWorker(extensionContext);
158181
} else {
159182
// Workflow stub
160-
String taskQueue = getTaskQueue(extensionContext);
161-
WorkflowOptions workflowOptions =
162-
WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build();
163-
return testEnvironment.getWorkflowClient().newWorkflowStub(parameterType, workflowOptions);
183+
return getTestEnvironment(extensionContext)
184+
.getWorkflowClient()
185+
.newWorkflowStub(parameterType, getWorkflowOptions(extensionContext));
164186
}
165187
}
166188

167189
@Override
168190
public void beforeEach(ExtensionContext context) throws Exception {
191+
long currentInitialTimeMillis =
192+
AnnotationSupport.findAnnotation(context.getElement(), WorkflowInitialTime.class)
193+
.map(annotation -> Instant.parse(annotation.value()).toEpochMilli())
194+
.orElse(initialTimeMillis);
169195
TestEnvironmentOptions testOptions =
170196
TestEnvironmentOptions.newBuilder()
171197
.setWorkflowClientOptions(workflowClientOptions)
172198
.setWorkerFactoryOptions(workerFactoryOptions)
173199
.setUseExternalService(useExternalService)
174200
.setTarget(target)
175-
.setInitialTimeMillis(initialTimeMillis)
201+
.setInitialTimeMillis(currentInitialTimeMillis)
176202
.build();
177203
TestWorkflowEnvironment testEnvironment = TestWorkflowEnvironment.newInstance(testOptions);
178204
String taskQueue =
@@ -187,7 +213,7 @@ public void beforeEach(ExtensionContext context) throws Exception {
187213

188214
setTestEnvironment(context, testEnvironment);
189215
setWorker(context, worker);
190-
setTaskQueue(context, taskQueue);
216+
setWorkflowOptions(context, WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build());
191217
}
192218

193219
@Override
@@ -219,12 +245,12 @@ private void setWorker(ExtensionContext context, Worker worker) {
219245
getStore(context).put(WORKER_KEY, worker);
220246
}
221247

222-
private String getTaskQueue(ExtensionContext context) {
223-
return getStore(context).get(TASK_QUEUE_KEY, String.class);
248+
private WorkflowOptions getWorkflowOptions(ExtensionContext context) {
249+
return getStore(context).get(WORKFLOW_OPTIONS_KEY, WorkflowOptions.class);
224250
}
225251

226-
private void setTaskQueue(ExtensionContext context, String taskQueue) {
227-
getStore(context).put(TASK_QUEUE_KEY, taskQueue);
252+
private void setWorkflowOptions(ExtensionContext context, WorkflowOptions taskQueue) {
253+
getStore(context).put(WORKFLOW_OPTIONS_KEY, taskQueue);
228254
}
229255

230256
private ExtensionContext.Store getStore(ExtensionContext context) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.testing;
21+
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/** Overrides the initial timestamp used by the {@link TestWorkflowExtension} */
28+
@Retention(RetentionPolicy.RUNTIME)
29+
@Target(ElementType.METHOD)
30+
public @interface WorkflowInitialTime {
31+
32+
String value();
33+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
3+
*
4+
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
5+
*
6+
* Modifications copyright (C) 2017 Uber Technologies, Inc.
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
9+
* use this file except in compliance with the License. A copy of the License is
10+
* located at
11+
*
12+
* http://aws.amazon.com/apache2.0
13+
*
14+
* or in the "license" file accompanying this file. This file is distributed on
15+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
16+
* express or implied. See the License for the specific language governing
17+
* permissions and limitations under the License.
18+
*/
19+
20+
package io.temporal.testing;
21+
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
24+
import io.temporal.activity.Activity;
25+
import io.temporal.activity.ActivityInterface;
26+
import io.temporal.activity.ActivityMethod;
27+
import io.temporal.activity.DynamicActivity;
28+
import io.temporal.common.converter.EncodedValues;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.extension.RegisterExtension;
31+
32+
public class TestActivityExtensionDynamicTest {
33+
34+
@RegisterExtension
35+
public static final TestActivityExtension activityExtension =
36+
TestActivityExtension.newBuilder()
37+
.setActivityImplementations(new MyDynamicActivityImpl())
38+
.build();
39+
40+
@ActivityInterface
41+
public interface MyActivity {
42+
43+
@ActivityMethod(name = "OverriddenActivityMethod")
44+
String activity1(String input);
45+
}
46+
47+
private static class MyDynamicActivityImpl implements DynamicActivity {
48+
49+
@Override
50+
public Object execute(EncodedValues args) {
51+
return Activity.getExecutionContext().getInfo().getActivityType()
52+
+ "-"
53+
+ args.get(0, String.class);
54+
}
55+
}
56+
57+
@Test
58+
public void extensionShouldResolveDynamicActivitiesParameters(MyActivity activity) {
59+
assertEquals("OverriddenActivityMethod-input1", activity.activity1("input1"));
60+
}
61+
}

temporal-testing-junit5/src/test/java/io/temporal/testing/ActivityExtensionTest.java renamed to temporal-testing-junit5/src/test/java/io/temporal/testing/TestActivityExtensionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.junit.jupiter.api.Test;
3030
import org.junit.jupiter.api.extension.RegisterExtension;
3131

32-
public class ActivityExtensionTest {
32+
public class TestActivityExtensionTest {
3333

3434
@RegisterExtension
3535
public static final TestActivityExtension activityExtension =

0 commit comments

Comments
 (0)