테스트 가능성 및 종속성 주입 - AWS Flow Framework 자바용

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

테스트 가능성 및 종속성 주입

이 프레임워크는 제어 반전(IoC)에 적합하게 설계되었습니다. 활동 및 워크플로 구현뿐 아니라 프레임워크에서 제공하는 작업자 및 컨텍스트 객체는 Spring과 같은 컨테이너를 사용하여 구성 및 인스턴스화할 수 있습니다. 즉시 사용 가능한 이 프레임워크는 Spring Framework와 통합됩니다. 뿐만 아니라 단위 테스트 워크플로 및 활동 구현을 위해 JUnit과의 통합도 지원합니다.

Spring 통합

com.amazonaws.services.simpleworkflow.flow.spring 패키지에는 사용자 애플리케이션에서 Spring 프레임워크를 쉽게 사용할 수 있게 해주는 클래스가 포함되어 있습니다. 이 클래스에는 사용자 지정 Scope 및 Spring 인식 활동 및 워크플로 작업자인 WorkflowScope, SpringWorkflowWorkerSpringActivityWorker이 포함됩니다. 이 클래스를 통해 사용자는 전적으로 Spring을 통해 작업자뿐 아니라 워크플로 및 활동 구현을 구성할 수 있습니다.

WorkflowScope

WorkflowScope는 프레임워크에서 제공하는 사용자 지정 Spring Scope 구현입니다. 이 범위를 통해 사용자는 수명 범위가 결정 작업의 수명으로 지정된 Spring 컨테이너에 객체를 생성할 수 있습니다. 이 범위의 빈(bean)은 작업자가 새 결정 작업을 수신할 때마다 인스턴스화됩니다. 사용자는 워크플로 구현 빈과 이 구현에서 의존하는 기타 빈에 대해 이 범위를 사용해야 합니다. 프레임워크에서는 각 결정 작업에 대해 새 빈을 생성할 것을 요구하기 때문에 Spring에서 제공하는 singleton과 프로토타입 범위를 워크플로 구현 빈에 사용해서는 안 됩니다. 이를 준수하지 않으면 예상치 못한 동작이 발생합니다.

다음 예시에서는 WorkflowScope를 등록한 후 이를 사용해 워크플로 구현 빈 및 활동 클라이언트 빈을 구성하는 Spring 구성의 코드 조각을 보여줍니다.

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

구성 관련 행: workflowImpl 빈의 구성에서 사용되는 <aop:scoped-proxy proxy-target-class="false" />가 필요한 이유는 WorkflowScope에서 CGLIB를 사용한 프록시 설정을 지원하지 않기 때문입니다. 사용자는 다른 범위에 있는 다른 빈에 연결된 WorkflowScope에 있는 모든 빈에 대해 이 구성을 사용해야 합니다. 이 경우 workflowImpl 빈은 singleton 범위 내에 있는 워크플로 작업자 빈에 연결해야 합니다(아래의 전체 예시 참조).

Spring Framework 설명서에서 사용자 지정 범위 사용에 관한 자세한 내용을 학습하실 수 있습니다.

Spring 인식 작업자

Spring 사용 시 사용자는 프레임워크에서 제공하는 Spring 인식 작업자 클래스인 SpringWorkflowWorkerSpringActivityWorker를 사용해야 합니다. 이 작업자는 다음 예시와 같이 Spring을 사용하여 애플리케이션에 주입될 수 있습니다. Spring 인식 작업자는 Spring의 SmartLifecycle 인터페이스를 구현하고, Spring 컨텍스트가 시작될 때 작업에 대해 폴링을 자동으로 시작하도록 기본 설정되어 있습니다. 사용자는 작업자의 disableAutoStartup 속성을 true로 설정하여 이 기능을 비활성화할 수 있습니다.

다음 예시에서는 결정자를 구성하는 방법을 보여줍니다. 이 예시에서는 MyActivitiesMyWorkflow 인터페이스(여기에는 표시되지 않음)와 그에 상응하는 구현인 MyActivitiesImplMyWorkflowImpl를 사용합니다. 생성된 클라이언트 인터페이스 및 구현은 MyWorkflowClient/MyWorkflowClientImplMyActivitiesClient/ 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"); } }

결정 컨텍스트 주입

워크플로 구현에서 컨텍스트 객체에 의존하는 경우 사용자는 역시 Spring을 통해 이 객체를 쉽게 주입할 수 있습니다. 프레임워크에서는 Spring 컨테이너에 컨텍스트 관련 빈을 자동으로 등록합니다. 예를 들어 다음 코드 조각에서는 여러 컨텍스트 객체가 자동 연결되었습니다. 컨텍스트 객체의 다른 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를 워크플로 구현에 주입하고 이를 사용하여 컨텍스트를 생성할 수 있습니다. 이 방법은 공급자 및 컨텍스트의 사용자 지정 구현을 제공하고 싶은 경우에 유용할 수 있습니다.

리소스 활동에 주입

제어 반전(IoC) 컨테이너를 사용하여 활동 구현을 인스턴스화하고 구성하며 데이터베이스 연결과 같은 리소스를 활동 구현 클래스의 속성으로 선언하여 주입할 수 있습니다. 그러한 리소스는 일반적으로 그 범위가 singleton으로 지정됩니다. 활동 구현은 여러 스레드에서 활동 작업자가 호출한다는 점에 유의하십시오. 따라서 공유된 리소스에 대한 액세스는 동기화되어야 합니다.

JUnit 통합

이 프레임워크에서는 JUnit으로 단위 테스트를 작성하고 실행하는 데 사용할 수 있는, 테스트 클록과 같은 컨텍스트 객체의 테스트 구현뿐 아니라 JUnit 확장도 제공합니다. 사용자는 이러한 확장을 사용해 워크플로 구현을 로컬로 인라인에서 테스트할 수 있습니다.

간단한 단위 테스트 작성

해당 워크플로에 대해 테스트를 작성하려면 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 헬프 클래스가 제공됩니다. 이 클래스를 통해 사용자는 약속이 준비 상태가 되길 기다렸다가 결과를 확인할 수 있습니다. 이 예시에서 우리는 워크플로 실행의 결과가 준비될 때까지 기다렸다가 테스트 결과를 확인합니다.

Spring을 사용하는 경우 WorkflowTest 클래스 대신에 SpringWorkflowTest 클래스를 사용할 수 있습니다. SpringWorkflowTest는 Spring 구성을 통해 활동 및 워크플로 구현을 쉽게 구성할 때 사용할 수 있는 속성을 제공합니다. Spring 인식 작업자와 마찬가지로 사용자는 WorkflowScope를 사용하여 워크플로 구현 빈을 구성해야 합니다. 이렇게 하면 각 결정 작업에 대해 새 워크플로 구현 빈이 하나씩 생성되도록 보장할 수 있습니다. 범위 지정된 프록시 프록시 대상 클래스 설정이 false로 설정되도록 이 빈을 구성해야 합니다. 자세한 내용은 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>

활동 구현 모의

테스트 중에 실제 활동 구현을 사용할 수 있지만, 단지 워크플로 로직을 단위 테스트하고 싶다면 활동을 모의해야 합니다. 이는 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); } }

또는 활동 클라이언트의 모의 구현을 제공하고 이를 워크플로 구현에 주입할 수 있습니다.

컨텍스트 객체 테스트

워크플로 구현이 프레임워크 컨텍스트 객체(예: DecisionContext)에 의존하는 경우 이러한 워크플로를 테스트하기 위해 특별한 작업을 수행하지 않아도 됩니다. 테스트가 WorkflowTest를 통해 실행되면 테스트 컨텍스트 객체를 자동으로 주입합니다. 워크플로 구현에서 컨텍스트 객체(예: DecisionContextProviderImpl 사용)에 액세스하면 테스트 구현을 진행합니다. 사용자는 테스트 코드(@Test 메서드)에서 이러한 테스트 컨텍스트 객체를 조작하여 흥미로운 테스트 사례를 만들 수 있습니다. 예를 들어 해당 워크플로에서 타이머를 생성하는 경우 사용자는 WorkflowTest 클래스에 있는 clockAdvanceSeconds 메서드를 호출하여 타이머를 활성화시켜 클록이 시간상 앞으로 이동하도록 할 수 있습니다. 또한 WorkflowTestClockAccelerationCoefficient 속성을 사용해 클록 속도를 높여 타이머가 평상시보다 더 일찍 활성화되게 할 수 있습니다. 예를 들어 해당 워크플로에서 한 시간짜리 타이머를 생성하는 경우 사용자는 ClockAccelerationCoefficient를 60으로 설정하여 타이머를 1분 내에 활성화시킬 수 있습니다. ClockAccelerationCoefficient는 1로 기본 설정됩니다.

com.amazonaws.services.simpleworkflow.flow.test 및 com.amazonaws.services.simpleworkflow.flow.junit 패키지에 관한 자세한 내용은 AWS SDK for Java 설명서를 참조하십시오.