토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 인티그레이션 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.


Spring Integration은 애플리케이션 테스트를 도와주는 다양한 유틸리티와 어노테이션을 제공하고 있다. 테스트 지원은 두 가지 모듈을 통해 이루어진다:

spring-integration-test-support(5.0 버전 이전에는 spring-integration-test)는 독립적으로 사용할 수 있는, unit 테스트를 위한 기본적인 유틸리티, rule, matcher를 제공한다. (Spring Integration 자체에 대한 의존성이 없으며, 프레임워크 내부 테스트에서 사용한다). spring-integration-test는 integration 테스트 지원을 위한 모듈로, 통합 구성 요소를 모킹하고, 전체 통합 플로우나 일부만 사용해서 개별 구성 요소들의 동작을 검증할 수 있는 종합적인 고수준 API를 제공한다.

엔터프라이즈 환경에서 빈틈없는 테스트를 준비하는 법은 이 레퍼런스 매뉴얼의 범위를 벗어난다. 가지고 있는 통합 솔루션을 테스트하기 위한 원칙이나 아이디어를 얻고자 한다면 Gregor Hohpe와 Wendy Istvanick가 작성한 “Test-Driven Development in Enterprise Integration Projects”를 읽어봐라.

Spring Integration의 테스트 프레임워크와 테스트 유틸리티는 전반적으로 기존의 JUnit, Hamcrest, Mockito같은 라이브러리를 사용한다. 애플리케이션 컨텍스트 상호작용은 스프링 테스트 프레임워크를 이용한다. 자세한 내용은 해당 프로젝트 문서를 참고해라.

Spring Integration 프레임워크을 이용하면, 표준 EIP 구현과 일급 객체first-class citizen(ex. MessageChannel, Endpoint, MessageHandler), 그리고 추상화와 느슨한 결합이라는 원칙 덕분에, 아무리 복잡한 통합 솔루션이라도 쉽게 구현할 수 있다. Spring Integration API를 사용해 플로우를 정의하면, (대부분) 통합 솔루션의 다른 구성 요소에 영향을 주지 않고 일부 플로우를 개선하고, 수정하고, 교체할 수 있다. 하지만 이러한 통합 솔루션을 테스트하는 것은 end-to-end로 접근하든, 별개로 접근하든 여전히 어려운 과제다. 기존에 있는 툴들을 활용하면 몇 가지 통합 프로토콜을 테스트하거나 모킹할 수 있으며, Spring Integration 채널 어댑터와도 잘 동작한다. 예를 들면 다음과 같은 툴을 활용할 수 있다:

이런 툴과 라이브러리는 대부분 Spring Integration 테스트에서도 사용하고 있다. 깃허브 레포지토리를 보면서 (각 모듈의 test 디렉토리), 통합 솔루션을 위한 자체 테스트를 어떻게 구성할지 생각해보는 것도 좋다.

이어서 이 챕터에선 Spring Integration이 제공하는 테스트 도구와 유틸리티에 대해 설명한다.

목차


G.1. Testing Utilities

spring-integration-test-support 모듈은 unit 테스트를 위한 유틸리티와 헬퍼 클래스를 제공한다.

G.1.1. TestUtils

TestUtils 클래스는 주로 다음과 같이 JUnit 테스트 안에서 프로퍼티를 검증할 때 사용한다:

@Test
public void loadBalancerRef() {
    MessageChannel channel = channels.get("lbRefChannel");
    LoadBalancingStrategy lbStrategy = TestUtils.getPropertyValue(channel,
                 "dispatcher.loadBalancingStrategy", LoadBalancingStrategy.class);
    assertTrue(lbStrategy instanceof SampleLoadBalancingStrategy);
}

TestUtils.getPropertyValue()는 스프링의 DirectFieldAccessor를 기반으로 동작하며, 타겟 private 프로퍼티의 값을 가져와준다. 앞의 예제와 같이 dot을 사용해 중첩 프로퍼티에도 접근할 수 있다.

팩토리 메소드 createTestApplicationContext()는 Spring Integration 환경에 맞는 TestApplicationContext 인스턴스를 생성해준다.

이 클래스를 자세히 알아보려면 Javadoc에서 다른 TestUtils 메소드들을 함께 읽어봐라.

G.1.2. Using OnlyOnceTrigger

OnlyOnceTrigger는 테스트 메시지를 딱 하나만 생성한 뒤, 이후 메시지에는 영향을 주지 않은 채로 엔드포인트를 폴링하고 동작을 검증해야 하는 경우에 활용할 수 있다. 다음은 OnlyOnceTrigger를 설정하는 예시다:

<bean id="testTrigger" class="org.springframework.integration.test.util.OnlyOnceTrigger" />

<int:poller id="jpaPoller" trigger="testTrigger">
    <int:transactional transaction-manager="transactionManager" />
</int:poller>

다음은 앞에서 설정한 OnlyOnceTrigger를 사용해 테스트를 진행하는 예시다:

@Autowired
@Qualifier("jpaPoller")
PollerMetadata poller;

@Autowired
OnlyOnceTrigger testTrigger;

@Test
@DirtiesContext
public void testWithEntityClass() throws Exception {
    this.testTrigger.reset();
    ...
    JpaPollingChannelAdapter jpaPollingChannelAdapter = new JpaPollingChannelAdapter(jpaExecutor);

    SourcePollingChannelAdapter adapter = JpaTestUtils.getSourcePollingChannelAdapter(
    		jpaPollingChannelAdapter, this.outputChannel, this.poller, this.context,
    		this.getClass().getClassLoader());
    adapter.start();
    ...
}

G.1.3. Support Components

org.springframework.integration.test.support 패키지엔 테스트 코드에서 직접 구현해서 사용해야 하는 다양한 추상 클래스가 담겨있다:

G.1.4. JUnit Rules and Conditions

JUnit 4의 테스트 rule LongRunningIntegrationTest은 환경 변수 또는 시스템 프로퍼티 RUN_LONG_INTEGRATION_TESTStrue로 설정돼있으면 테스트를 실행해야 함을 나타내는 rule이다. 그 외에는 테스트를 건너뛴다. 5.1 버전부터 JUnit 5 테스트에도 같은 용도로 사용할 수 있는 조건부 어노테이션 @LongRunningTest를 제공한다.

G.1.5. Hamcrest and Mockito Matchers

org.springframework.integration.test.matcher 패키지에는 unit 테스트 안에서 Message와 그 프로퍼티들을 검증하기 위한 여러 가지 Matcher 구현체가 들어있다. 다음은 matcher 한 가지의 사용법을 보여주는 예시다 (PayloadMatcher):

import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload;
...
@Test
public void transform_withFilePayload_convertedToByteArray() throws Exception {
    Message<?> result = this.transformer.transform(message);
    assertThat(result, is(notNullValue()));
    assertThat(result, hasPayload(is(instanceOf(byte[].class))));
    assertThat(result, hasPayload(SAMPLE_CONTENT.getBytes(DEFAULT_ENCODING)));
}

다음과 같이 스터빙stubbing을 위한 mock 객체와 검증에는 팩토리 클래스 MockitoMessageMatchers를 사용할 수 있다:

static final Date SOME_PAYLOAD = new Date();

static final String SOME_HEADER_VALUE = "bar";

static final String SOME_HEADER_KEY = "test.foo";
...
Message<?> message = MessageBuilder.withPayload(SOME_PAYLOAD)
                .setHeader(SOME_HEADER_KEY, SOME_HEADER_VALUE)
                .build();
MessageHandler handler = mock(MessageHandler.class);
handler.handleMessage(message);
verify(handler).handleMessage(messageWithPayload(SOME_PAYLOAD));
verify(handler).handleMessage(messageWithPayload(is(instanceOf(Date.class))));
...
MessageChannel channel = mock(MessageChannel.class);
when(channel.send(messageWithHeaderEntry(SOME_HEADER_KEY, is(instanceOf(Short.class)))))
        .thenReturn(true);
assertThat(channel.send(message), is(false));

G.1.6. AssertJ conditions and predicates

5.2 버전부터 AssertJ의 matches() 검증에 사용할 수 있는 MessagePredicate를 도입했다. 이 클래스에선 기대값으로 Message 객체가 필요하다. 또한 기대값이나 실제 메시지 검증에서 제외할 헤더를 설정할 수도 있다.


G.2. Spring Integration and the Test Context

일반적으로 스프링 애플리케이션을 테스트할 땐 스프링 테스트 프레임워크를 사용한다. Spring Integration은 스프링 프레임워크가 토대이기 때문에, 스프링 테스트 프레임워크에서 가능한 것은 전부 통합 플로우를 테스트할 때도 적용할 수 있다. org.springframework.integration.test.context 패키지는 통합 요구 사항에 맞게 테스트 컨텍스트를 업그레이드해주는 몇 가지 구성 요소들을 제공한다. 우선 다음 예제와 같이 테스트 클래스에 @SpringIntegrationTest 어노테이션을 설정해 Spring Integration 테스트 프레임워크를 활성화해주자:

@RunWith(SpringRunner.class)
@SpringIntegrationTest(noAutoStartup = {"inboundChannelAdapter", "*Source*"})
public class MyIntegrationTests {

    @Autowired
    private MockIntegrationContext mockIntegrationContext;

}

@SpringIntegrationTest 어노테이션은 MockIntegrationContext 빈을 등록해주기 때문에, 테스트 클래스에 주입하면 MockIntegrationContext의 메소드에 액세스할 수 있다. Spring Integration 테스트 프레임워크는 noAutoStartup 옵션을 지정했다면 autoStartup=true인 엔드포인트들을 시작하지 않는다. 이 엔드포인트들은 지정한 패턴과 매칭해서 찾는데, xxx*, *xxx*, *xxx, xxx*yyy와 같은 간단한 패턴 스타일을 지원한다.

이 옵션은 인바운드 채널 어댑터가 타겟 시스템과 실제로 커넥션을 맺는 것을 원하지 않을 때 유용하다 (ex. AMQP 인바운드 게이트웨이, JDBC 폴링 채널 어댑터, 클라이언트 모드의 WebSocket 메시지 Producer 등).

테스트 케이스에서 실제 애플리케이션 컨텍스트에 있는 빈을 수정해야 할 땐 MockIntegrationContext를 사용하면 된다. 예를 들어, autoStartupfalse로 재정의한 엔드포인트들은 다음과 같이 mock 객체로 대체할 수 있다:

@Test
public void testMockMessageSource() {
    MessageSource<String> messageSource = () -> new GenericMessage<>("foo");

    this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint", messageSource);

    Message<?> receive = this.results.receive(10_000);
    assertNotNull(receive);
}

여기서 mySourceEndpoint는 실제 MessageSource를 mock으로 대체하는 SourcePollingChannelAdapter의 빈 이름을 나타낸다. 유사하게 MockIntegrationContext.substituteMessageHandlerFor()MessageHandler를 엔드포인트로 래핑하는 IntegrationConsumer 빈의 이름을 받는다.

테스트를 수행한 후 MockIntegrationContext.resetBeans()를 사용하면 엔드포인트 빈의 상태를 실제 설정으로 복원할 수 있다:

@After
public void tearDown() {
    this.mockIntegrationContext.resetBeans();
}

자세한 내용은 Javadoc을 참고해라.


G.3. Integration Mocks

org.springframework.integration.test.mock 패키지는 Spring Integration 구성 요소의 움직임을 모킹mocking, 스터빙stubbing, 검증하기 위한 도구와 유틸리티를 제공한다. 모킹 기능은 유명 테스트 프레임워크 Mockito를 기반으로 동작하며, 완벽하게 호환된다. (현재 Mockito의 전이 의존성transitive dependency은 2.5.x 이상을 사용한다.)

G.3.1. MockIntegration

팩토리 클래스 MockIntegration은 통합 플로우를 구성하는 Spring Integration 빈(MessageSource, MessageProducer, MessageHandler, MessageChannel)에 대한 mock 객체를 빌드할 수 있는 API를 제공한다. 다음 예제와 같이 설정 단계 뿐 아니라 테스트 메소드 안에서도 mock을 사용해 검증하기 전 실제 엔드포인트 교체할 수 있다:

<int:inbound-channel-adapter id="inboundChannelAdapter" channel="results">
    <bean class="org.springframework.integration.test.mock.MockIntegration" factory-method="mockMessageSource">
        <constructor-arg value="a"/>
        <constructor-arg>
            <array>
                <value>b</value>
                <value>c</value>
            </array>
        </constructor-arg>
    </bean>
</int:inbound-channel-adapter>

다음은 위와 동일한 설정을 구성하는 자바 코드다:

@InboundChannelAdapter(channel = "results")
@Bean
public MessageSource<Integer> testingMessageSource() {
    return MockIntegration.mockMessageSource(1, 2, 3);
}
...
StandardIntegrationFlow flow = IntegrationFlows
        .from(MockIntegration.mockMessageSource("foo", "bar", "baz"))
        .<String, String>transform(String::toUpperCase)
        .channel(out)
        .get();
IntegrationFlowRegistration registration = this.integrationFlowContext.registration(flow)
        .register();

그러려면 다음과 같이 앞에서 설명한 MockIntegrationContext를 사용해서 테스트해야 한다:

this.mockIntegrationContext.substituteMessageSourceFor("mySourceEndpoint",
        MockIntegration.mockMessageSource("foo", "bar", "baz"));
Message<?> receive = this.results.receive(10_000);
assertNotNull(receive);
assertEquals("FOO", receive.getPayload());

Mockito의 MessageSource mock 객체와 달리는, MockMessageHandler는 메시지를 받아 스터빙 처리하는 체인 API를 가지고 있는 일반적인 AbstractMessageProducingHandler의 하위 클래스다. MockMessageHandler는 다음 요청 메시지를 받으면 실행할 단방향 스터빙 동작을 지정할 수 있는 handleNext(Consumer<Message<?>>)를 제공한다. 이 메소드는 응답을 생성하지 않는 메시지 핸들러를 모킹할 때 사용한다. handleNextAndReply(Function<Message<?>, ?>)는 다음 요청 메시지를 받으면 동일하게 스터빙 로직을 실행하고, 이에 대한 응답을 생성한다. 이 메소드들을 체이닝하면 예상할 수 있는 모든 요청 메시지에 대해 임의의 request-reply 시나리오를 시뮬레이션해볼 수 있다. 이러한 컨슈머와 함수는 누적해놨다가 한 번에 하나씩 꺼내서 사용하며, 남은 메시지들엔 전부 마지막 항목을 적용한다. 이 동작은 Mockito의 AnswerdoReturn() API와 유사하다.

또한 생성자 인자를 통해 MockMessageHandler에 Mockito ArgumentCaptor<Message<?>>를 제공할 수 있다. 그러면 이 ArgumentCaptorMockMessageHandler에 대한 각 요청 메시지를 포착한다. 테스트 중에 getValue(), getAllValues() 메소드를 사용하면 해당 요청 메시지를 확인하고 검증할 수 있다.

MockIntegrationContext는 테스트 중인 엔드포인트에서 실제로 설정한 MessageHandlerMockMessageHandler로 대체할 수 있는 substituteMessageHandlerFor() API를 제공한다.

다음은 이를 활용한 전형적인 테스트 시나리오 예시다:

ArgumentCaptor<Message<?>> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class);

MessageHandler mockMessageHandler =
        mockMessageHandler(messageArgumentCaptor)
                .handleNextAndReply(m -> m.getPayload().toString().toUpperCase());

this.mockIntegrationContext.substituteMessageHandlerFor("myService.serviceActivator",
                               mockMessageHandler);
GenericMessage<String> message = new GenericMessage<>("foo");
this.myChannel.send(message);
Message<?> received = this.results.receive(10000);
assertNotNull(received);
assertEquals("FOO", received.getPayload());
assertSame(message, messageArgumentCaptor.getValue());

자세한 내용은 MockIntegrationMockMessageHandler의 Javadoc을 참고해라.


G.4. Other Resources

프레임워크 안에 들어있는 테스트 케이스를 탐색해봐도 좋지만, Spring Integration Samples 레포지토리에는 testing-examples, advanced-testing-examples같이 테스트 코드를 보여주기 위해 특별히 만들어둔 샘플 애플리케이션이 포함돼있다. file-split-ftp 샘플과 같이, 경우에 따라서 샘플 자체에 종합적인 end-to-end 테스트가 담겨있기도 하다.


Next :
Appendix I. Additional Resources
스프링 인티그레이션과 관련된 정보와 소식을 확인할 수 있는 사이트 소개

전체 목차는 여기에 있습니다.

<< >>

TOP