Testability and Dependency Injection
The framework is designed to be Inversion of Control (IoC) friendly. Activity and workflow implementations as well as the framework supplied workers and context objects can be configured and instantiated using containers like Spring. Out of the box, the framework provides integration with the Spring Framework. In addition, integration with JUnit has been provided for unit testing workflow and activity implementations.
Spring Integration
The com.amazonaws.services.simpleworkflow.flow.spring package contains classes that make it easy to use the
Spring framework in your applications. These include a custom Scope and Spring-aware activity and workflow
workers: WorkflowScope
, SpringWorkflowWorker
and SpringActivityWorker
.
These classes allow you to configure your workflow and activity implementations as well as the workers entirely
through Spring.
WorkflowScope
WorkflowScope
is a custom Spring Scope implementation provided by the framework. This
scope allows you to create objects in the Spring container whose lifetime is scoped to
that of a decision task. The beans in this scope are instantiated every time a new
decision task is received by the worker. You should use this scope for workflow
implementation beans and any other beans it depends on. The Spring-provided singleton and
prototype scopes should not be used for workflow implementation beans because the
framework requires that a new bean be created for each decision task. Failure to do so
will result in unexpected behavior.
The following example shows a snippet of Spring configuration that registers the
WorkflowScope
and then uses it for configuring a workflow implementation bean and an activity
client bean.
<!-- register AWS Flow Framework for Java WorkflowScope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <!-- activities client --> <bean id="activitiesClient" class="aws.flow.sample.MyActivitiesClientImpl" scope="workflow"> </bean> <!-- workflow implementation --> <bean id="workflowImpl" class="aws.flow.sample.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient"/> <aop:scoped-proxy proxy-target-class="false" /> </bean>
The line of configuration: <aop:scoped-proxy proxy-target-class="false" />
, used in the
configuration of the workflowImpl
bean, is required because the WorkflowScope
doesn't
support proxying using CGLIB. You should use this configuration for any bean in the WorkflowScope
that
is wired to another bean in a different scope. In this case, the workflowImpl
bean needs to be wired to
a workflow worker bean in singleton scope (see complete example below).
You can learn more about using custom scopes in the Spring Framework documentation.
Spring-Aware Workers
When using Spring, you should use the Spring-aware worker classes provided by the framework:
SpringWorkflowWorker
and SpringActivityWorker
. These workers can be injected in your
application using Spring as shown in the next example. The Spring-aware workers implement Spring's
SmartLifecycle
interface and, by default, automatically start polling for tasks when the Spring
context is initialized. You can turn off this functionality by setting the disableAutoStartup
property of the worker to true
.
The following example shows how to configure a decider. This example uses MyActivities
and
MyWorkflow
interfaces (not shown here) and corresponding implementations,
MyActivitiesImpl
and MyWorkflowImpl
. The generated client interfaces and implementations
are MyWorkflowClient
/MyWorkflowClientImpl
and
MyActivitiesClient
/MyActivitiesClientImpl
(also not shown here).
The activities client is injected in the workflow implementation using Spring's auto wire feature:
public class MyWorkflowImpl implements MyWorkflow { @Autowired public MyActivitiesClient client; @Override public void start() { client.activity1(); } }
The Spring configuration for the decider is as follows:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- register custom workflow scope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <context:annotation-config/> <bean id="accesskeys" class="com.amazonaws.auth.BasicAWSCredentials"> <constructor-arg value="{AWS.Access.ID}"/> <constructor-arg value="{AWS.Secret.Key}"/> </bean> <bean id="clientConfiguration" class="com.amazonaws.ClientConfiguration"> <property name="socketTimeout" value="70000" /> </bean> <!-- Amazon SWF client --> <bean id="swfClient" class="com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient"> <constructor-arg ref="accesskeys" /> <constructor-arg ref="clientConfiguration" /> <property name="endpoint" value="{service.url}" /> </bean> <!-- activities client --> <bean id="activitiesClient" class="aws.flow.sample.MyActivitiesClientImpl" scope="workflow"> </bean> <!-- workflow implementation --> <bean id="workflowImpl" class="aws.flow.sample.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient"/> <aop:scoped-proxy proxy-target-class="false" /> </bean> <!-- workflow worker --> <bean id="workflowWorker" class="com.amazonaws.services.simpleworkflow.flow.spring.SpringWorkflowWorker"> <constructor-arg ref="swfClient" /> <constructor-arg value="domain1" /> <constructor-arg value="tasklist1" /> <property name="registerDomain" value="true" /> <property name="domainRetentionPeriodInDays" value="1" /> <property name="workflowImplementations"> <list> <ref bean="workflowImpl" /> </list> </property> </bean> </beans>
Since the SpringWorkflowWorker
is fully configured in Spring and automatically starts
polling when the Spring context is initialized, the host process for the decider is
simple:
public class WorkflowHost { public static void main(String[] args){ ApplicationContext context = new FileSystemXmlApplicationContext("resources/spring/WorkflowHostBean.xml"); System.out.println("Workflow worker started"); } }
Similarly, the activity worker can be configured as follows:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- register custom scope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <bean id="accesskeys" class="com.amazonaws.auth.BasicAWSCredentials"> <constructor-arg value="{AWS.Access.ID}"/> <constructor-arg value="{AWS.Secret.Key}"/> </bean> <bean id="clientConfiguration" class="com.amazonaws.ClientConfiguration"> <property name="socketTimeout" value="70000" /> </bean> <!-- Amazon SWF client --> <bean id="swfClient" class="com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient"> <constructor-arg ref="accesskeys" /> <constructor-arg ref="clientConfiguration" /> <property name="endpoint" value="{service.url}" /> </bean> <!-- activities impl --> <bean name="activitiesImpl" class="asadj.spring.test.MyActivitiesImpl"> </bean> <!-- activity worker --> <bean id="activityWorker" class="com.amazonaws.services.simpleworkflow.flow.spring.SpringActivityWorker"> <constructor-arg ref="swfClient" /> <constructor-arg value="domain1" /> <constructor-arg value="tasklist1" /> <property name="registerDomain" value="true" /> <property name="domainRetentionPeriodInDays" value="1" /> <property name="activitiesImplementations"> <list> <ref bean="activitiesImpl" /> </list> </property> </bean> </beans>
The activity worker host process is similar to the decider:
public class ActivityHost { public static void main(String[] args) { ApplicationContext context = new FileSystemXmlApplicationContext( "resources/spring/ActivityHostBean.xml"); System.out.println("Activity worker started"); } }
Injecting Decision Context
If your workflow implementation depends on the context objects, then you can easily inject them through Spring as well. The framework automatically registers context-related beans in the Spring container. For example, in the following snippet, the various context objects have been auto wired. No other Spring configuration of the context objects is required.
public class MyWorkflowImpl implements MyWorkflow { @Autowired public MyActivitiesClient client; @Autowired public WorkflowClock clock; @Autowired public DecisionContext dcContext; @Autowired public GenericActivityClient activityClient; @Autowired public GenericWorkflowClient workflowClient; @Autowired public WorkflowContext wfContext; @Override public void start() { client.activity1(); } }
If you want to configure the context objects in the workflow implementation through Spring
XML configuration, then use the bean names declared in the WorkflowScopeBeanNames
class in the
com.amazonaws.services.simpleworkflow.flow.spring package. For example:
<!-- workflow implementation --> <bean id="workflowImpl" class="asadj.spring.test.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient"/> <property name="clock" ref="workflowClock"/> <property name="activityClient" ref="genericActivityClient"/> <property name="dcContext" ref="decisionContext"/> <property name="workflowClient" ref="genericWorkflowClient"/> <property name="wfContext" ref="workflowContext"/> <aop:scoped-proxy proxy-target-class="false" /> </bean>
Alternatively, you may inject a DecisionContextProvider
in the workflow implementation
bean and use it to create the context. This can be useful if you want to provide custom
implementations of the provider and context.
Injecting Resources in Activities
You can instantiate and configure activity implementations using an Inversion of Control (IoC) container and easily inject resources like database connections by declaring them as properties of the activity implementation class. Such resources will typically be scoped as singletons. Note that activity implementations are called by the activity worker on multiple threads. Therefore, access to shared resources must be synchronized.
JUnit Integration
The framework provides JUnit extensions as well as test implementations of the context objects, such as a test clock, that you can use to write and run unit tests with JUnit. With these extensions, you can test your workflow implementation locally inline.
Writing a Simple Unit Test
In order to write tests for your workflow, use the WorkflowTest
class in the
com.amazonaws.services.simpleworkflow.flow.junit package. This class is a
framework-specific JUnit MethodRule
implementation and runs your workflow code locally,
calling activities inline as opposed to going through Amazon SWF. This gives you the
flexibility to run your tests as frequently as you desire without incurring any
charges.
In order to use this class, simply declare a field of type WorkflowTest
and
annotate it with the @Rule
annotation. Before running your tests, create a
new WorkflowTest
object and add your activity and workflow implementations to
it. You can then use the generated workflow client factory to create a client and start an
execution of the workflow. The framework also provides a custom JUnit runner,
FlowBlockJUnit4ClassRunner
, that you must use for your workflow tests. For
example:
@RunWith(FlowBlockJUnit4ClassRunner.class) public class BookingWorkflowTest { @Rule public WorkflowTest workflowTest = new WorkflowTest(); List<String> trace; private BookingWorkflowClientFactory workflowFactory = new BookingWorkflowClientFactoryImpl(); @Before public void setUp() throws Exception { trace = new ArrayList<String>(); // Register activity implementation to be used during test run BookingActivities activities = new BookingActivitiesImpl(trace); workflowTest.addActivitiesImplementation(activities); workflowTest.addWorkflowImplementationType(BookingWorkflowImpl.class); } @After public void tearDown() throws Exception { trace = null; } @Test public void testReserveBoth() { BookingWorkflowClient workflow = workflowFactory.getClient(); Promise<Void> booked = workflow.makeBooking(123, 345, true, true); List<String> expected = new ArrayList<String>(); expected.add("reserveCar-123"); expected.add("reserveAirline-123"); expected.add("sendConfirmation-345"); AsyncAssert.assertEqualsWaitFor("invalid booking", expected, trace, booked); } }
You can also specify a separate task list for each activity implementation that you add
to WorkflowTest
. For example, if you have a workflow implementation that schedules
activities in host-specific task lists, then you can register the activity in the task
list of each host:
for (int i = 0; i < 10; i++) { String hostname = "host" + i; workflowTest.addActivitiesImplementation(hostname, new ImageProcessingActivities(hostname)); }
Notice that the code in the @Test
is asynchronous. Therefore, you should use the
asynchronous workflow client to start an execution. In order to verify the results of your
test, an AsyncAssert
help class is also provided. This class allows you to wait for
promises to become ready before verifying results. In this example, we wait for the result
of the workflow execution to be ready before verifying the test output.
If you are using Spring, then the SpringWorkflowTest
class can be used instead
of the WorkflowTest
class. SpringWorkflowTest
provides
properties that you can use to configure activity and workflow implementations easily
through Spring configuration. Just like the Spring-aware workers, you should use the
WorkflowScope
to configure workflow implementation beans. This ensures that
a new workflow implementation bean is created for every decision task. Make sure to
configure these beans with the scoped-proxy proxy-target-class setting set to
false
. See the Spring Integration section for more details. The example
Spring configuration shown in the Spring Integration section can be changed to test the
workflow using SpringWorkflowTest
:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans ht tp://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframe work.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <!-- register custom workflow scope --> <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="workflow"> <bean class="com.amazonaws.services.simpleworkflow.flow.spring.WorkflowScope" /> </entry> </map> </property> </bean> <context:annotation-config /> <bean id="accesskeys" class="com.amazonaws.auth.BasicAWSCredentials"> <constructor-arg value="{AWS.Access.ID}" /> <constructor-arg value="{AWS.Secret.Key}" /> </bean> <bean id="clientConfiguration" class="com.amazonaws.ClientConfiguration"> <property name="socketTimeout" value="70000" /> </bean> <!-- Amazon SWF client --> <bean id="swfClient" class="com.amazonaws.services.simpleworkflow.AmazonSimpleWorkflowClient"> <constructor-arg ref="accesskeys" /> <constructor-arg ref="clientConfiguration" /> <property name="endpoint" value="{service.url}" /> </bean> <!-- activities client --> <bean id="activitiesClient" class="aws.flow.sample.MyActivitiesClientImpl" scope="workflow"> </bean> <!-- workflow implementation --> <bean id="workflowImpl" class="aws.flow.sample.MyWorkflowImpl" scope="workflow"> <property name="client" ref="activitiesClient" /> <aop:scoped-proxy proxy-target-class="false" /> </bean> <!-- WorkflowTest --> <bean id="workflowTest" class="com.amazonaws.services.simpleworkflow.flow.junit.spring.SpringWorkflowTest"> <property name="workflowImplementations"> <list> <ref bean="workflowImpl" /> </list> </property> <property name="taskListActivitiesImplementationMap"> <map> <entry> <key> <value>list1</value> </key> <ref bean="activitiesImplHost1" /> </entry> </map> </property> </bean> </beans>
Mocking Activity Implementations
You may use the real activity implementations during testing, but if you want to unit test
just the workflow logic, you should mock the activities. This can do this by providing a
mock implementation of the activities interface to the WorkflowTest
class.
For example:
@RunWith(FlowBlockJUnit4ClassRunner.class) public class BookingWorkflowTest { @Rule public WorkflowTest workflowTest = new WorkflowTest(); List<String> trace; private BookingWorkflowClientFactory workflowFactory = new BookingWorkflowClientFactoryImpl(); @Before public void setUp() throws Exception { trace = new ArrayList<String>(); // Create and register mock activity implementation to be used during test run BookingActivities activities = new BookingActivities() { @Override public void sendConfirmationActivity(int customerId) { trace.add("sendConfirmation-" + customerId); } @Override public void reserveCar(int requestId) { trace.add("reserveCar-" + requestId); } @Override public void reserveAirline(int requestId) { trace.add("reserveAirline-" + requestId); } }; workflowTest.addActivitiesImplementation(activities); workflowTest.addWorkflowImplementationType(BookingWorkflowImpl.class); } @After public void tearDown() throws Exception { trace = null; } @Test public void testReserveBoth() { BookingWorkflowClient workflow = workflowFactory.getClient(); Promise<Void> booked = workflow.makeBooking(123, 345, true, true); List<String> expected = new ArrayList<String>(); expected.add("reserveCar-123"); expected.add("reserveAirline-123"); expected.add("sendConfirmation-345"); AsyncAssert.assertEqualsWaitFor("invalid booking", expected, trace, booked); } }
Alternatively, you can provide a mock implementation of the activities client and inject that into your workflow implementation.
Test Context Objects
If your workflow implementation depends on the framework context objects—for example,
the DecisionContext
—you don't have to do anything special to test such
workflows. When a test is run through WorkflowTest
, it automatically injects
test context objects. When your workflow implementation accesses the context
objects—for example, using DecisionContextProviderImpl
—it will
get the test implementation. You can manipulate these test context objects in your test
code (@Test
method) to create interesting test cases. For example, if your
workflow creates a timer, you can make the timer fire by calling the
clockAdvanceSeconds
method on the WorkflowTest
class to move
the clock forward in time. You can also accelerate the clock to make timers fire earlier
than they normally would using the ClockAccelerationCoefficient
property on
WorkflowTest
. For example, if your workflow creates a timer for one hour,
you can set the ClockAccelerationCoefficient
to 60 to make the timer fire in
one minute. By default, ClockAccelerationCoefficient
is set to 1.
For more details about the com.amazonaws.services.simpleworkflow.flow.test and com.amazonaws.services.simpleworkflow.flow.junit packages, see the AWS SDK for Java documentation.