

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# 可試性與相依性插入
<a name="test"></a>

**Topics**
+ [Spring 整合](#test.spring)
+ [JUnit 整合](#test.junit)

框架設計旨在容易控制反轉 (IoC)。活動與工作流程實作以及框架提供的工作者和內容物件，都可以使用如 Spring 的容器予以設定和執行個體化。框架可立即提供與 Spring Framework 的整合。此外，也已提供與 JUnit 的整合，進行工作流程和活動實作的單元測試。

## Spring 整合
<a name="test.spring"></a>

com.amazonaws.services.simpleworkflow.flow.spring 套裝服務包含的類別，方便您在應用程式中使用 Spring 框架。這些包括自訂的 Scope 和 Spring 感知活動及工作流程工作者：`WorkflowScope`、`SpringWorkflowWorker` 和 `SpringActivityWorker`。這些類別讓您完全透過 Spring 設定您的工作流程和活動實作以及工作者。

### WorkflowScope
<a name="test.workflowscope"></a>

`WorkflowScope` 是框架提供的自訂 Spring Scope 實作。此範圍讓您在生命週期不超過決策任務生命週期的 Spring 容器中建立物件。每次工作者收到決策任務時，在此範圍內的 Bean 都會執行個體化。您應該為工作流程實作 Bean 及其相依的任何其他 Bean 使用此範圍。Spring 提供的單一和原型範圍不應用於工作流程實作 Bean，因為框架需要為每項決策任務建立新的 Bean。無法執行此作業會造成未預期的行為。

下例示範 Spring 組態的程式碼片段，註冊 `WorkflowScope` 後，用它設定工作流程實作 Bean 和活動用戶端 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>
```

組態程式碼：`<aop:scoped-proxy proxy-target-class="false" />`，用於 `workflowImpl` Bean 的組態，為必要項目，因為 `WorkflowScope` 不支援使用 CGLIB 做為代理。您應該為 `WorkflowScope` 中所有接到不同範圍中其他 Bean 的 Bean 使用此組態。在本例中，`workflowImpl` Bean 需要接到單一範圍中的工作流程工作者 Bean (請參閱以下的完整範例)。

您可以在 Spring Framework 文件中深入了解使用自訂的範圍。

### Spring 感知工作者
<a name="test.springworkers"></a>

使用 Spring 時，您應該使用框架提供的 Spring 感知工作者類別：`SpringWorkflowWorker` 和 `SpringActivityWorker`。這些工作者可使用 Spring 插入您的應用程式，如下一個範例所示。Spring 感知工作者預設會實作 Spring 的 `SmartLifecycle` 界面，在 Spring 內容初始化時自動開始輪詢任務。您可以將工作者的 `disableAutoStartup` 屬性設成 `true` 來關閉此功能。

下列範例示範如何設定決策者。本例使用 `MyActivities` 和 `MyWorkflow` 界面 (此處不顯示) 以及對應的實作 `MyActivitiesImpl` 和 `MyWorkflowImpl`。產生的用戶端界面和實作為 `MyWorkflowClient`/`MyWorkflowClientImpl` 和 `MyActivitiesClient`/`MyActivitiesClientImpl` (此處也不顯示)。

活動用戶端會使用 Spring 的自動接線功能插入到工作流程實作中：

```
public class MyWorkflowImpl implements MyWorkflow {
   @Autowired
   public MyActivitiesClient client;

   @Override
   public void start() {
      client.activity1();
   }
}
```

決策者的 Spring 組態如下所示：

```
<?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>
```

由於 `SpringWorkflowWorker` 已在 Spring 中完整設定，並在 Spring 內容初始化時自動開始輪詢，因此決策者的主機程序很簡單：

```
public class WorkflowHost {
   public static void main(String[] args){
      ApplicationContext context
          = new FileSystemXmlApplicationContext("resources/spring/WorkflowHostBean.xml");
      System.out.println("Workflow worker started");
   }
}
```

同樣地，活動工作者可設定如下：

```
<?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>
```

活動工作者裝載程序類似決策者：

```
public class ActivityHost {
   public static void main(String[] args) {
      ApplicationContext context = new FileSystemXmlApplicationContext(
      "resources/spring/ActivityHostBean.xml");
      System.out.println("Activity worker started");
   }
}
```

### 插入決策內容
<a name="test.injectdecision"></a>

如果您的工作流程實作依賴這些內容物件，您也可以透過 Spring 輕鬆插入它們。框架自動在 Spring 容器中註冊與內容相關的 Bean。例如，在下列程式碼片段中，已自動接線各種內容物件。不需要內容物件的其他 Spring 組態。

```
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();
   }
}
```

如果您想要透過 Spring XML 組態在工作流程實作中設定內容物件，請在 com.amazonaws.services.simpleworkflow.flow.spring 套裝服務中使用 `WorkflowScopeBeanNames` 類別中宣告的 Bean 名稱。例如：

```
<!-- 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>
```

或者，您可將 `DecisionContextProvider` 插入到工作流程實作 Bean 中，用它建立內容。如果您想提供自訂的提供者和內容實作，這會很有用。

### 在活動中插入資源
<a name="test.injectresource"></a>

您可以使用控制反轉 (IoC) 容器執行個體化活動實作並予以設定，然後將資源宣告為活動實作類別的屬性，輕鬆插入資料庫連線等資源。這類資源一般範圍限定為單一個。請注意，活動實作在多執行緒上是由活動工作者呼叫。因此，共享資源的存取必須予以同步。

## JUnit 整合
<a name="test.junit"></a>

框架提供 JUnit 延伸以及內容物件的測試實作 (例如測試時鐘)，您可用來撰寫及執行搭配 JUnit 的單元測試。使用這些延伸，您可以在本機測試您的內嵌工作流程實作。

### 撰寫簡單的單元測試
<a name="test.junit.simple"></a>

 為針對您的工作流程撰寫測試，請使用 com.amazonaws.services.simpleworkflow.flow.junit 套裝服務中的 `WorkflowTest` 類別。此類別是架構特定的 JUnit `MethodRule`實作，並在本機執行您的工作流程程式碼，呼叫內嵌活動而不是通過 Amazon SWF。這可讓您彈性執行測試，不限次數，也不產生任何費用。

為使用此類別，只要宣告 `WorkflowTest` 類型的欄位並標記以 `@Rule` 註釋即可。執行您的測試之前，請先建立新的 `WorkflowTest` 物件，並將您的活動和工作流程實作新增至此物件。然後，您可以使用產生的工作流程用戶端服務團隊來建立用戶端並開始執行工作流程。框架也提供自訂的 JUnit 執行器 `FlowBlockJUnit4ClassRunner`，您必須將它用於您的工作流程測試中。例如：

```
@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);
    }
}
```

您也可以為每一個新增至 `WorkflowTest` 的活動實作另行指定任務清單。例如，如果您的工作流程實作會在主機特定的任務清單中排程活動，您就可以在每部主機的任務清單中註冊活動：

```
for (int i = 0; i < 10; i++) {
    String hostname = "host" + i;
    workflowTest.addActivitiesImplementation(hostname,
                                             new ImageProcessingActivities(hostname));
}
```

請注意，`@Test` 中的程式碼為非同步。因此，您應該使用非同步工作流程用戶端來啟動執行。為驗證您的測試結果，也會提供 `AsyncAssert` 協助類別。此類別允許您等候 Promise 就緒再驗證結果。在本例中，我們會先等候工作流程執行的結果就緒，再驗證測試輸出。

如果您要使用 Spring，則可以使用 `SpringWorkflowTest` 類別，而不是 `WorkflowTest` 類別。`SpringWorkflowTest` 提供的屬性，可讓您輕鬆透過 Spring 組態用來設定您的活動和工作流程實作。就像 Spring 感知工作者一樣，您應該使用 `WorkflowScope` 來設定工作流程實作 Bean。這可確保為每一項決策任務建立新的工作流程實作 Bean。確定設定範圍限定代理 proxy-target-class 設定設為 `false` 的這些 Bean。如需詳細資訊，請參閱「Spring 整合」一節。在「Spring 整合」一節中示範的 Spring 組態範例可變更為使用 `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>
```

#### 模擬活動實作
<a name="test.junit.mockactivity"></a>

您可以在測試期間使用真實的活動實作，但若只想要對工作流程邏輯進行單元測試，您應該模擬活動。將活動界面的模擬實作提供給 `WorkflowTest` 類別，即可執行此作業。例如：

```
@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);
    }
}
```

或者，您可以提供活動用戶端的模擬實作，將它插入您的工作流程實作中。

### 測試內容物件
<a name="test.junit.objects"></a>

如果您的工作流程實作取決於架構內容物件，例如 `DecisionContext`，則您不需要執行任何特殊動作來測試此類工作流程。透過 `WorkflowTest` 執行測試時，它會自動插入測試內容物件。當您的工作流程實作存取內容物件時，例如使用 `DecisionContextProviderImpl`，它將取得測試實作。您可在您的測試程式碼中 (`@Test` 方法) 操控這些測試內容物件，建立有趣的測試案例。例如，如果您的工作流程建立計時器，您可以在 `WorkflowTest` 類別上呼叫 `clockAdvanceSeconds` 方法建立計時器觸發，向前撥動時鐘的時間。您也可以在 `WorkflowTest` 上使用 `ClockAccelerationCoefficient` 屬性，加速時鐘，建立比平常早的計時器觸發。例如，如果您的工作流程建立一小時的計時器，您可以將 `ClockAccelerationCoefficient` 設成 60，建立一分鐘的計時器觸發。`ClockAccelerationCoefficient` 預設會設定為 1。

如需 com.amazonaws.services.simpleworkflow.flow.test 和 com.amazonaws.services.simpleworkflow.flow.junit 套裝服務的詳細資訊，請參閱 適用於 Java 的 AWS SDK 文件。