Testabilité et injection de dépendances - AWS Flow Framework pour Java

Les traductions sont fournies par des outils de traduction automatique. En cas de conflit entre le contenu d'une traduction et celui de la version originale en anglais, la version anglaise prévaudra.

Testabilité et injection de dépendances

L'infrastructure est conçue pour être compatible avec l'inversion de contrôle (IoC). Les implémentations d'activité et de flux de travail ainsi que les exécuteurs et les objets de contexte fournis par l'infrastructure peuvent être configurés et instanciés via des conteneurs comme Spring. Par défaut, l'infrastructure permet une intégration avec le framework Spring. En outre, une intégration avec le framework JUnit est fournie en vue d'un test unitaire des implémentations de flux de travail et d'activité.

Intégration de Spring

Le package com.amazonaws.services.simpleworkflow.flow.spring contient des classes qui facilitent l'utilisation du framework Spring dans vos applications. Ces classes incluent une portée (Scope) personnalisée et des exécuteurs d'activité et de flux de travail compatibles avec Spring : WorkflowScope, SpringWorkflowWorker et SpringActivityWorker. Ces classes vous permettent de configurer totalement via Spring vos implémentations d'activité et de flux de travail ainsi que les exécuteurs.

WorkflowScope

WorkflowScope est une implémentation de portée (Scope) Spring personnalisée fournie par l'infrastructure. Cette portée vous permet de créer des objets dans le conteneur Spring dont la durée de vie dépend de celle d'une tâche de décision. Les beans de cette portée sont instanciés chaque fois qu'une nouvelle tâche de décision est reçue par l'exécuteur. Vous devez utiliser cette portée pour les beans d'implémentation de flux de travail et pour tous les autres beans dont elle dépend. Les portées singleton et prototype fournies par Spring ne doivent pas être utilisées pour les beans d'implémentation de flux de travail car l'infrastructure requiert qu'un nouveau bean soit crée pour chaque tâche de décision. Si vous ne respectez pas cette règle, vous risquez d'obtenir des comportements inattendus.

L'exemple suivant présente un extrait de configuration Spring qui enregistre la portée WorkflowScope puis l'utilise pour la configuration d'un bean d'implémentation de flux de travail et d'un bean de client d'activité.

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

La ligne de configuration <aop:scoped-proxy proxy-target-class="false" />, utilisée dans la configuration du bean workflowImpl, est obligatoire car la portée WorkflowScope ne prend pas en charge la mise en place de proxy avec CGLIB. Vous devez utiliser cette configuration pour tout bean de la portée WorkflowScope qui est lié à un autre bean d'une autre portée. Dans ce cas, le bean workflowImpl a besoin d'être lié à un bean d'objet exécuteur de flux de travail dans une portée singleton (reportez-vous à l'exemple complet ci-dessous).

Vous trouverez de plus amples informations sur l'utilisation de portées personnalisées dans la documentation du framework Spring.

Exécuteurs compatibles avec Spring

Lorsque vous utilisez Spring, vous devez utiliser les classes d'exécuteur compatibles avec Spring fournies par l'infrastructure : SpringWorkflowWorker et SpringActivityWorker. Ces exécuteurs peuvent être injectés dans votre application en utilisant Spring comme décrit dans l'exemple suivant. Les exécuteurs compatibles avec Spring implémentent l'interface SmartLifecycle de Spring et, par défaut, démarrent automatiquement la recherche des tâches lors de l'initialisation du contexte Spring. Vous pouvez désactiver cette fonctionnalité en définissant la propriété disableAutoStartup de l'exécuteur sur true.

L'exemple suivant montre comment configurer un décideur. Cet exemple utilise les interfaces MyActivities et MyWorkflow (non présentées ici) et les implémentations correspondantes, MyActivitiesImpl et MyWorkflowImpl. Les interfaces et les implémentations de client générées sont MyWorkflowClient/MyWorkflowClientImpl et MyActivitiesClient/MyActivitiesClientImpl (également non présentées ici).

Le client des activités est injecté dans l'implémentation du flux de travail via la fonction auto wire (liaison automatique) de Spring :

public class MyWorkflowImpl implements MyWorkflow { @Autowired public MyActivitiesClient client; @Override public void start() { client.activity1(); } }

La configuration Spring pour le décideur se présente comme suit :

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

Étant donné que l'élément SpringWorkflowWorker est totalement configuré dans Spring et qu'il démarre automatiquement la recherche des tâches lors de l'initialisation du contexte Spring, le processus hôte du décideur est simple :

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

De même, l'exécuteur d'activité peut être configuré comme suit :

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

Le processus hôte de l'exécuteur d'activité est similaire au décideur :

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

Injection de contexte décisionnel

Si l'implémentation de votre flux de travail dépend des objets de contexte, vous pouvez facilement les injecter via Spring. L'infrastructure enregistre automatiquement les beans liés au contexte dans le conteneur Spring. Par exemple, dans l'extrait suivant, les divers objets de contexte ont été liés automatiquement via la fonction auto wire. Aucune autre configuration Spring des objets de contexte n'est requise.

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

Si vous souhaitez configurer les objets de contexte dans l'implémentation de flux de travail via la configuration XML Spring, utilisez les noms de beans déclarés dans la classe WorkflowScopeBeanNames dans le package com.amazonaws.services.simpleworkflow.flow.spring. Par Exemple:

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

Sinon, vous pouvez injecter un élément DecisionContextProvider dans le bean d'implémentation de flux de travail et l'utiliser pour créer le contexte. Cela peut être utile si vous souhaitez fournir des implémentations personnalisées du fournisseur et du contexte.

Injection des ressources dans des activités

Vous pouvez instancier et configurer des implémentations d'activité en utilisant un conteneur d'inversion de contrôle (IoC) et injecter facilement des ressources telles que des connexions de bases de données en les déclarant en tant que propriétés de la classe d'implémentation d'activité. Ces ressources sont généralement définies comme des portées de type singleton. Notez que les implémentations d'activité sont appelées par l'exécuteur d'activité sur plusieurs threads. L'accès aux ressources partagées doit donc être synchronisé.

Intégration de JUnit

L'infrastructure fournit des extensions JUnit ainsi que des implémentations de test des objets de contexte, tels qu'une horloge de test, que vous pouvez utiliser pour écrire et exécuter des tests unitaires avec JUnit. Ces extensions vous permettent de tester l'implémentation de votre flux de travail localement en ligne.

Écriture d'un test unitaire simple

Pour écrire des tests pour votre flux de travail, utilisez la classe WorkflowTest du package com.amazonaws.services.simpleworkflow.flow.junit. Cette classe est un JUnit spécifique au frameworkMethodRuleimplémentation et exécute votre code de flux de travail localement, en appelant les activités en ligne par opposition à Amazon SWF. Cela vous permet d'exécuter vos tests aussi souvent que vous le souhaitez, sans encourir aucun frais.

Pour utiliser cette classe, déclarez simplement un champ de type WorkflowTest et annotez-le avec l'annotation @Rule. Avant d'exécuter vos tests, créez un nouvel objet WorkflowTest et ajoutez-lui vos implémentations d'activité et de flux de travail. Vous pouvez ensuite utiliser la fabrique de clients de flux de travail générée pour créer un client et lancer l'exécution du flux de travail. L'infrastructure fournit également un exécuteur JUnit personnalisé, FlowBlockJUnit4ClassRunner, que vous devez utiliser pour vos tests de flux de travail. Par Exemple:

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

Vous pouvez également spécifier une liste de tâches distincte pour chaque implémentation d'activité que vous ajoutez à WorkflowTest. Par exemple, si vous avez une implémentation de flux de travail qui planifie des activités dans des listes de tâches propres à chaque hôte, vous pouvez enregistrer l'activité dans la liste de tâches de chaque hôte :

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

Notez que le code dans @Test est asynchrone. Vous devez donc utiliser le client de flux de travail asynchrone pour lancer une exécution. Pour vérifier les résultats de votre test, une classe d'aide AsyncAssert est également fournie. Cette classe vous permet d'attendre que les objets attendus passent à l'état prêt avant de vérifier les résultats. Dans cet exemple, nous attendons que le résultat de l'exécution du flux de travail soit prêt pour vérifier la sortie du test.

Si vous utilisez Spring, la classe SpringWorkflowTest peut être utilisé au lieu de la classe WorkflowTest. SpringWorkflowTest fournit les propriétés que vous pouvez utiliser pour configurer des implémentations d'activité et de flux de travail facilement via la configuration de Spring. Tout comme vous pourriez le faire avec les exécuteurs compatibles Spring, vous devez utiliser la portée WorkflowScope pour configurer les beans d'implémentation de flux de travail. Cela permet de s'assurer qu'un nouveau bean d'implémentation de flux de travail est créé pour chaque tâche de décision. Veillez à configurer ces beans avec le paramètre scoped-proxy proxy-target-class défini sur false. Pour plus d'informations, consultez la section Intégration de Spring. L'exemple de configuration Spring présenté dans la section Intégration de Spring peut être modifié pour tester le flux de travail à l'aide de 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>

Simulation d'implémentations d'activité

Vous pouvez utiliser des implémentations d'activité réelles pendant les tests, mais si vous souhaitez effectuer un test unitaire uniquement sur la logique de flux de travail, vous devez simuler les activités. Vous pouvez le faire en fournissant une implémentation factice de l'interface d'activités à la classe WorkflowTest. Par Exemple:

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

Sinon, vous pouvez aussi fournir une implémentation factice du client d'activités et l'injecter dans l'implémentation de votre flux de travail.

Objets de contexte de test

Si l'implémentation de votre flux de travail dépend des objets de contexte d'infrastructure, par exemple, l'objetDecisionContext: vous n'avez pas besoin d'effectuer d'opérations particulières pour tester ce type de flux de travail. Lorsqu'un test est exécuté via WorkflowTest, il injecte automatiquement les objets de contexte de test. Lorsque l'implémentation de votre flux de travail accède aux objets de contexte, par exemple viaDecisionContextProviderImpl—il obtiendra l'implémentation du test. Vous pouvez manipuler ces objets de contexte de test dans votre code de test (méthode @Test) pour créer des cas de test intéressants. Par exemple, si votre flux de travail crée un temporisateur, vous pouvez faire en sorte qu'il se déclenche en appelant la méthode clockAdvanceSeconds sur la classe WorkflowTest pour déclencher l'horloge. Vous pouvez également accélérer l'horloge afin que les temporisateurs se déclenchent plus tôt qu'ils ne le feraient normalement en utilisant la propriété ClockAccelerationCoefficient sur WorkflowTest. Par exemple, si votre flux de travail crée un temporisateur pour un heure, vous pouvez définir le coefficient ClockAccelerationCoefficient sur 60 afin que le temporisateur se déclenche au bout d'une minute. Par défaut, ClockAccelerationCoefficient est défini sur 1.

Pour plus d'informations sur les packages com.amazonaws.services.simpleworkflow.flow.test et com.amazonaws.services.simpleworkflow.flow.junit, consultez la documentation AWS SDK for Java.