Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

package io.temporal.testing;

import io.temporal.activity.DynamicActivity;
import io.temporal.common.metadata.POJOActivityImplMetadata;
import io.temporal.common.metadata.POJOActivityInterfaceMetadata;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.extension.AfterEachCallback;
Expand Down Expand Up @@ -67,13 +69,19 @@ public class TestActivityExtension

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

private boolean includesDynamicActivity;

private TestActivityExtension(Builder builder) {
testEnvironmentOptions = builder.testEnvironmentOptions;
activityImplementations = builder.activityImplementations;

supportedParameterTypes.add(TestActivityEnvironment.class);

for (Object activity : activityImplementations) {
if (DynamicActivity.class.isAssignableFrom(activity.getClass())) {
includesDynamicActivity = true;
continue;
}
POJOActivityImplMetadata metadata = POJOActivityImplMetadata.newInstance(activity.getClass());
for (POJOActivityInterfaceMetadata activityInterface : metadata.getActivityInterfaces()) {
supportedParameterTypes.add(activityInterface.getInterfaceClass());
Expand All @@ -90,13 +98,31 @@ public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {

if (parameterContext.getParameter().getDeclaringExecutable() instanceof Constructor) {
Parameter parameter = parameterContext.getParameter();
if (parameter.getDeclaringExecutable() instanceof Constructor) {
// Constructor injection is not supported
return false;
}

Class<?> parameterType = parameterContext.getParameter().getType();
return supportedParameterTypes.contains(parameterType);
Class<?> parameterType = parameter.getType();
if (supportedParameterTypes.contains(parameterType)) {
return true;
}

if (!includesDynamicActivity) {
// If no DynamicActivity implementation was registered then supportedParameterTypes are the
// only ones types that can be injected
return false;
}

try {
// If POJOActivityInterfaceMetadata can be instantiated then parameterType is a proper
// activity interface and can be injected
POJOActivityInterfaceMetadata.newInstance(parameterType);
return true;
} catch (Exception e) {
return false;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
import io.temporal.worker.Worker;
import io.temporal.worker.WorkerFactoryOptions;
import io.temporal.worker.WorkerOptions;
import io.temporal.workflow.DynamicWorkflow;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
Expand All @@ -40,6 +42,7 @@
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestWatcher;
import org.junit.platform.commons.support.AnnotationSupport;

/**
* JUnit Jupiter extension that simplifies testing of Temporal workflows.
Expand Down Expand Up @@ -77,7 +80,7 @@ public class TestWorkflowExtension

private static final String TEST_ENVIRONMENT_KEY = "testEnvironment";
private static final String WORKER_KEY = "worker";
private static final String TASK_QUEUE_KEY = "taskQueue";
private static final String WORKFLOW_OPTIONS_KEY = "workflowOptions";

private final WorkerOptions workerOptions;
private final WorkflowClientOptions workflowClientOptions;
Expand All @@ -90,6 +93,7 @@ public class TestWorkflowExtension
private final long initialTimeMillis;

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

private TestWorkflowExtension(Builder builder) {
workerOptions = builder.workerOptions;
Expand All @@ -113,6 +117,10 @@ private TestWorkflowExtension(Builder builder) {
supportedParameterTypes.add(Worker.class);

for (Class<?> workflowType : workflowTypes) {
if (DynamicWorkflow.class.isAssignableFrom(workflowType)) {
includesDynamicWorkflow = true;
continue;
}
POJOWorkflowImplMetadata metadata = POJOWorkflowImplMetadata.newInstance(workflowType);
for (POJOWorkflowInterfaceMetadata workflowInterface : metadata.getWorkflowInterfaces()) {
supportedParameterTypes.add(workflowInterface.getInterfaceClass());
Expand All @@ -129,50 +137,68 @@ public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {

if (parameterContext.getParameter().getDeclaringExecutable() instanceof Constructor) {
Parameter parameter = parameterContext.getParameter();
if (parameter.getDeclaringExecutable() instanceof Constructor) {
// Constructor injection is not supported
return false;
}

Class<?> parameterType = parameterContext.getParameter().getType();
return supportedParameterTypes.contains(parameterType);
Class<?> parameterType = parameter.getType();
if (supportedParameterTypes.contains(parameterType)) {
return true;
}

if (!includesDynamicWorkflow) {
// If no DynamicWorkflow implementation was registered then supportedParameterTypes are the
// only ones types that can be injected
return false;
}

try {
// If POJOWorkflowInterfaceMetadata can be instantiated then parameterType is a proper
// workflow interface and can be injected
POJOWorkflowInterfaceMetadata.newInstance(parameterType);
return true;
} catch (Exception e) {
return false;
}
}

@Override
public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {

TestWorkflowEnvironment testEnvironment = getTestEnvironment(extensionContext);

Class<?> parameterType = parameterContext.getParameter().getType();
if (parameterType == TestWorkflowEnvironment.class) {
return testEnvironment;
return getTestEnvironment(extensionContext);
} else if (parameterType == WorkflowClient.class) {
return testEnvironment.getWorkflowClient();
return getTestEnvironment(extensionContext).getWorkflowClient();
} else if (parameterType == WorkflowOptions.class) {
String taskQueue = getTaskQueue(extensionContext);
return WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build();
return getWorkflowOptions(extensionContext);
} else if (parameterType == Worker.class) {
return getWorker(extensionContext);
} else {
// Workflow stub
String taskQueue = getTaskQueue(extensionContext);
WorkflowOptions workflowOptions =
WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build();
return testEnvironment.getWorkflowClient().newWorkflowStub(parameterType, workflowOptions);
return getTestEnvironment(extensionContext)
.getWorkflowClient()
.newWorkflowStub(parameterType, getWorkflowOptions(extensionContext));
}
}

@Override
public void beforeEach(ExtensionContext context) throws Exception {
long currentInitialTimeMillis =
AnnotationSupport.findAnnotation(context.getElement(), WorkflowInitialTime.class)
.map(annotation -> Instant.parse(annotation.value()).toEpochMilli())
.orElse(initialTimeMillis);
TestEnvironmentOptions testOptions =
TestEnvironmentOptions.newBuilder()
.setWorkflowClientOptions(workflowClientOptions)
.setWorkerFactoryOptions(workerFactoryOptions)
.setUseExternalService(useExternalService)
.setTarget(target)
.setInitialTimeMillis(initialTimeMillis)
.setInitialTimeMillis(currentInitialTimeMillis)
.build();
TestWorkflowEnvironment testEnvironment = TestWorkflowEnvironment.newInstance(testOptions);
String taskQueue =
Expand All @@ -187,7 +213,7 @@ public void beforeEach(ExtensionContext context) throws Exception {

setTestEnvironment(context, testEnvironment);
setWorker(context, worker);
setTaskQueue(context, taskQueue);
setWorkflowOptions(context, WorkflowOptions.newBuilder().setTaskQueue(taskQueue).build());
}

@Override
Expand Down Expand Up @@ -219,12 +245,12 @@ private void setWorker(ExtensionContext context, Worker worker) {
getStore(context).put(WORKER_KEY, worker);
}

private String getTaskQueue(ExtensionContext context) {
return getStore(context).get(TASK_QUEUE_KEY, String.class);
private WorkflowOptions getWorkflowOptions(ExtensionContext context) {
return getStore(context).get(WORKFLOW_OPTIONS_KEY, WorkflowOptions.class);
}

private void setTaskQueue(ExtensionContext context, String taskQueue) {
getStore(context).put(TASK_QUEUE_KEY, taskQueue);
private void setWorkflowOptions(ExtensionContext context, WorkflowOptions taskQueue) {
getStore(context).put(WORKFLOW_OPTIONS_KEY, taskQueue);
}

private ExtensionContext.Store getStore(ExtensionContext context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.testing;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Overrides the initial timestamp used by the {@link TestWorkflowExtension} */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface WorkflowInitialTime {

String value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved.
*
* Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Modifications copyright (C) 2017 Uber Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the License is
* located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package io.temporal.testing;

import static org.junit.jupiter.api.Assertions.assertEquals;

import io.temporal.activity.Activity;
import io.temporal.activity.ActivityInterface;
import io.temporal.activity.ActivityMethod;
import io.temporal.activity.DynamicActivity;
import io.temporal.common.converter.EncodedValues;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class TestActivityExtensionDynamicTest {

@RegisterExtension
public static final TestActivityExtension activityExtension =
TestActivityExtension.newBuilder()
.setActivityImplementations(new MyDynamicActivityImpl())
.build();

@ActivityInterface
public interface MyActivity {

@ActivityMethod(name = "OverriddenActivityMethod")
String activity1(String input);
}

private static class MyDynamicActivityImpl implements DynamicActivity {

@Override
public Object execute(EncodedValues args) {
return Activity.getExecutionContext().getInfo().getActivityType()
+ "-"
+ args.get(0, String.class);
}
}

@Test
public void extensionShouldResolveDynamicActivitiesParameters(MyActivity activity) {
assertEquals("OverriddenActivityMethod-input1", activity.activity1("input1"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class ActivityExtensionTest {
public class TestActivityExtensionTest {

@RegisterExtension
public static final TestActivityExtension activityExtension =
Expand Down
Loading