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

스프링 클라우드 컨트랙트 공식 레퍼런스를 한글로 번역한 문서입니다.

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


Spring Cloud Contract를 사용하면 메시지를 통해 통신하는 애플리케이션을 검증할 수 있다. 이 문서에서 언급하는 모든 통합 기능은 스프링을 기반으로 동작하지만, 원한다면 직접 만든 구현체를 사용할 수도 있다.

목차

3.5.1. Messaging DSL Top-level Elements

메시지 처리를 위한 DSL은 HTTP 전용 DSL과는 조금 다르게 생겼다. 그 차이점들은 아래 섹션에서 나누어 설명한다:

Output Triggered by a Method

아래 예시에서 처럼, 특정 메소드를 호출해 메시지 출력을 트리거할 수 있다 (명세contract가 시작되는 시점이나, 메시지를 전송한 시점의 Scheduler 등으로):

Groovy YAML
def dsl = Contract.make {
	// Human readable description
	description 'Some description'
	// Label by means of which the output message can be triggered
	label 'some_label'
	// input to the contract
	input {
		// the contract will be triggered by a method
		triggeredBy('bookReturnedTriggered()')
	}
	// output message of the contract
	outputMessage {
		// destination to which the output message will be sent
		sentTo('output')
		// the body of the output message
		body('''{ "bookName" : "foo" }''')
		// the headers of the output message
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}
# Human readable description
description: Some description
# Label by means of which the output message can be triggered
label: some_label
input:
  # the contract will be triggered by a method
  triggeredBy: bookReturnedTriggered()
# output message of the contract
outputMessage:
  # destination to which the output message will be sent
  sentTo: output
  # the body of the output message
  body:
    bookName: foo
  # the headers of the output message
  headers:
    BOOK-NAME: foo

위 예제에서는 bookReturnedTriggered라는 메소드가 호출되면 output으로 출력 메시지를 전송한다. 메시지 프로듀서publisher는 이 메소드를 호출해 메시지를 트리거하는 테스트를 생성한다. 컨슈머consumersome_label을 사용해 메시지를 트리거할 수 있다.

Consumer/Producer

이 섹션에서 설명하는 내용은 Groovy DSL에만 해당하는 내용이다.

HTTP에선 client/stub, server/test라는 개념이 존재한다. 메시지를 처리할 때에도 유사한 개념을 사용할 수 있다. 또한 Spring Cloud Contract Verifier는 consumerproducer 메소드도 제공한다 ($ 또는 value 메소드를 사용해 consumerproducer 값을 따로 정의할 수 있다).

Common

input이나 outputMessage 섹션에서 assertThat을 호출하려면, 베이스 클래스에 정의한 메소드나 스태틱 임포트로 가져온 메소드를 이용하면 된다 (e.g. assertThatMessageIsOnTheQueueue()). Spring Cloud Contract가 자동 생성한 테스트에선 이 메소드를 호출한다.

3.5.2. Integrations

통합 설정은 다음 중 하나를 사용할 수 있다:

여기서는 스프링 부트를 사용하고 있기 때문에, 클래스패스에 이 라이브러리 중 하나를 추가하면, 메시지 처리에 필요한 모든 설정이 자동으로 세팅된다.

자동 생성 테스트에서 사용할 베이스 클래스에 @AutoConfigureMessageVerifier를 추가하는 것을 잊지말자. 이 어노테이션을 선언하지 않으면 Spring Cloud Contract의 메시지 처리 기능이 동작하지 않는다.

Spring Cloud Stream을 사용하고 싶다면, 다음과 같이 org.springframework.cloud:spring-cloud-stream에 대한 테스트 의존성을 추가해줘야 한다:

Maven Gradle
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-stream</artifactId>
      <type>test-jar</type>
      <scope>test</scope>
      <classifier>test-binder</classifier>
  </dependency>
  

Manual Integration Testing

테스트에서 사용하는 주요 인터페이스는 org.springframework.cloud.contract.verifier.messaging.MessageVerifierSenderorg.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver다. 각각은 메시지를 보내고 받는 방법을 정의한다.

테스트 코드에서 ContractVerifierMessageExchange를 주입받으면 명세contract에 맞는 메시지를 주고 받을 수 있다. 그런 다음 테스트에 @AutoConfigureMessageVerifier를 추가한다. 다음 예제를 참고해라:

@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {

  @Autowired
  private MessageVerifier verifier;
  ...
}

테스트에서 스텁stub도 필요하다면, 메시지 처리 설정이 포함된 @AutoConfigureStubRunner 하나만 추가해주면 된다.

3.5.3. Producer Side Messaging Test Generation

DSL에 input이나 outputMessage 섹션을 정의하면 프로듀서publisher 측에서 테스트가 생성된다. 기본적으로 JUnit 4 테스트를 생성하지만, JUnit 5나 TestNG, Spock 테스트도 생성할 수 있다.

messageFrom이나 sentTo로 넘기는 목적지destination는 메시지 처리 구현체에 따라 다른 의미를 가질 수 있다. Stream과 Integration의 경우, 먼저 채널의 destination으로 리졸브해본다. destination을 찾지 못했다면 그 다음엔 채널 이름으로 리졸브한다. Camel의 경우, 이는 특정 컴포넌트를 의미한다 (e.g. jms).

아래 명세contract를 살펴보자:

Groovy YAML
def contractDsl = Contract.make {
	name "foo"
	label 'some_label'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('activemq:output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
			messagingContentType(applicationJson())
		}
	}
}
label: some_label
input:
  triggeredBy: bookReturnedTriggered
outputMessage:
  sentTo: activemq:output
  body:
    bookName: foo
  headers:
    BOOK-NAME: foo
    contentType: application/json

위 예제에선 다음과 같은 테스트가 만들어진다:

JUnit Spock
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;

public class FooTest {
	@Inject ContractVerifierMessaging contractVerifierMessaging;
	@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;

	@Test
	public void validate_foo() throws Exception {
		// when:
			bookReturnedTriggered();

		// then:
			ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
					contract(this, "foo.yml"));
			assertThat(response).isNotNull();

		// and:
			assertThat(response.getHeader("BOOK-NAME")).isNotNull();
			assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
			assertThat(response.getHeader("contentType")).isNotNull();
			assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");

		// and:
			DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
			assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
	}

}
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging

import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes

class FooSpec extends Specification {
	@Inject ContractVerifierMessaging contractVerifierMessaging
	@Inject ContractVerifierObjectMapper contractVerifierObjectMapper

	def validate_foo() throws Exception {
		when:
			bookReturnedTriggered()

		then:
			ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output",
					contract(this, "foo.yml"))
			response != null

		and:
			response.getHeader("BOOK-NAME") != null
			response.getHeader("BOOK-NAME").toString() == 'foo'
			response.getHeader("contentType") != null
			response.getHeader("contentType").toString() == 'application/json'

		and:
			DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
			assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
	}

}

3.5.4. Consumer Stub Generation

HTTP와는 달리, 메시지를 처리할 땐 명세contract 정의를 스텁stub과 함께 JAR 안에 포함시켜야 한다. 그러면 컨슈머consumer 측에서 명세contract 정의를 파싱하고, 적절한 스텁stub 라우트를 세팅할 수 있다.

클래스패스에 프레임워크가 여러 개 있다면, Stub Runner에서 어떤 프레임워크를 사용할지를 정의해줘야 한다. 클래스패스 상에 AMQP, Spring Cloud Stream, Spring Integration이 존재하는데, Spring AMQP를 사용하고 싶다고 가정해보자. 이땐 stubrunner.stream.enabled=false, stubrunner.integration.enabled=false를 설정해야 한다. 이렇게 설정하면 Spring AMQP만 남게된다.

Stub triggering

메시지를 트리거할 땐 아래 보이는 StubTrigger 인터페이스를 활용해라:

import java.util.Collection;
import java.util.Map;

/**
 * Contract for triggering stub messages.
 *
 * @author Marcin Grzejszczak
 */
public interface StubTrigger {

	/**
	 * Triggers an event by a given label for a given {@code groupid:artifactid} notation.
	 * You can use only {@code artifactId} too.
	 *
	 * Feature related to messaging.
	 * @param ivyNotation ivy notation of a stub
	 * @param labelName name of the label to trigger
	 * @return true - if managed to run a trigger
	 */
	boolean trigger(String ivyNotation, String labelName);

	/**
	 * Triggers an event by a given label.
	 *
	 * Feature related to messaging.
	 * @param labelName name of the label to trigger
	 * @return true - if managed to run a trigger
	 */
	boolean trigger(String labelName);

	/**
	 * Triggers all possible events.
	 *
	 * Feature related to messaging.
	 * @return true - if managed to run a trigger
	 */
	boolean trigger();

	/**
	 * Feature related to messaging.
	 * @return a mapping of ivy notation of a dependency to all the labels it has.
	 */
	Map<String, Collection<String>> labels();

}

StubFinder 인터페이스는 StubTrigger를 상속하고 있기 때문에, 테스트에서는 둘 중 하나만 사용하면 된다.

StubTrigger를 사용하면 다음과 같은 방법으로 메시지를 트리거할 수 있다:

Trigger by Label

다음은 레이블로 메시지를 트리거하는 예시다:

stubFinder.trigger('return_book_1')

Trigger by Group and Artifact IDs

다음은 그룹 ID와 아티팩트 ID로 메시지를 트리거하는 예시다:

stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:streamService', 'return_book_1')

Trigger by Artifact IDs

다음은 아티팩트 ID로 메시지를 트리거하는 예시다:

stubFinder.trigger('streamService', 'return_book_1')

Trigger All Messages

다음은 모든 메시지를 트리거하는 예시다:

stubFinder.trigger()

3.5.5. Consumer Side Messaging With Apache Camel

Spring Cloud Contract Stub Runner의 메시지 처리 모듈은 Apache Camel과 쉽게 통합할 수 있는 방법을 제공한다. 아티팩트를 제공하면 자동으로 스텁stub을 다운로드하고 필요한 라우트를 등록해준다.

Adding Apache Camel to the Project

클래스패스에 Apache Camel과 Spring Cloud Contract Stub Runner를 모두 추가하면 된다. 테스트 클래스에 @AutoConfigureStubRunner를 선언하는 것을 잊지 말자.

Disabling the Functionality

이 기능을 비활성화해야 한다면 프로퍼티에 stubrunner.camel.enabled=false를 설정해라.

Examples

다음과 같이 메이븐 레포지토리에 camelService 애플리케이션에 대한 스텁stub을 배포했다고 가정해보자:

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── camelService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── camelService-0.0.1-SNAPSHOT.pom
                            │   ├── camelService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

그리고 스텁stub은 다음과 같은 구조를 가지고 있다고 가정한다:

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   └── bookReturned1.groovy
    └── mappings

이제 아래 명세contract를 살펴보자:

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('rabbitmq:output?queue=output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

return_book_1 레이블을 가진 메시지를 트리거하려면 StubTrigger 인터페이스를 다음과 같이 호출한다:

stubFinder.trigger("return_book_1")

이렇게 하면 명세contract의 출력 메시지에 정의한 목적지로 메시지를 전송한다.

3.5.6. Consumer Side Messaging with Spring Integration

Spring Cloud Contract Stub Runner의 메시지 처리 모듈은 Spring Integration과 쉽게 통합할 수 있는 방법을 제공한다. 아티팩트를 제공하면 자동으로 스텁stub을 다운로드하고 필요한 라우트를 등록해준다.

Adding the Runner to the Project

클래스패스에 Spring Integration과 Spring Cloud Contract Stub Runner를 모두 추가하면 된다. 테스트 클래스에 @AutoConfigureStubRunner를 선언하는 것을 잊지 말자.

Disabling the Functionality

이 기능을 비활성화해야 한다면 프로퍼티에 stubrunner.integration.enabled=false를 설정해라.

Examples

다음과 같이 메이븐 레포지토리에 integrationService 애플리케이션에 대한 스텁stub을 배포했다고 가정해보자:

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── integrationService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── integrationService-0.0.1-SNAPSHOT.pom
                            │   ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

그리고 스텁stub은 다음과 같은 구조를 가지고 있다고 가정한다:

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   └── bookReturned1.groovy
    └── mappings

이제 아래 명세contract를 살펴보자:

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

다음 Spring Integration Route도 함께 살펴보자:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
			 xmlns:beans="http://www.springframework.org/schema/beans"
			 xmlns="http://www.springframework.org/schema/integration"
			 xsi:schemaLocation="http://www.springframework.org/schema/beans
			https://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/integration
			http://www.springframework.org/schema/integration/spring-integration.xsd">


	<!-- REQUIRED FOR TESTING -->
	<bridge input-channel="output"
			output-channel="outputTest"/>

	<channel id="outputTest">
		<queue/>
	</channel>

</beans:beans>

return_book_1 레이블을 가진 메시지를 트리거하려면 StubTrigger 인터페이스를 다음과 같이 호출한다:

stubFinder.trigger('return_book_1')

이렇게 하면 명세contract의 출력 메시지에 정의한 목적지로 메시지를 전송한다.

3.5.7. Consumer Side Messaging With Spring Cloud Stream

Spring Cloud Contract Stub Runner의 메시지 처리 모듈은 Spring Stream과 쉽게 통합할 수 있는 방법을 제공한다. 아티팩트를 제공하면 자동으로 스텁stub을 다운로드하고 필요한 라우트를 등록해준다.

Stub Runner를 Stream과 통합할 땐, messageFrom이나 sentTo로 넘긴 문자열을 먼저 채널의 destination으로 리졸브해본다. destination을 찾지 못했다면 그 다음엔 채널 이름으로 리졸브한다.

Spring Cloud Stream을 사용하고 싶다면, 다음과 같이 org.springframework.cloud:spring-cloud-stream에 대한 테스트 의존성을 추가해줘야 한다:

Maven Gradle
<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-stream-test-binder</artifactId>
      <scope>test</scope>
  </dependency>
  

Adding the Runner to the Project

클래스패스에 Spring Cloud Stream과 Spring Cloud Contract Stub Runner를 모두 추가하면 된다. 테스트 클래스에 @AutoConfigureStubRunner를 선언하는 것을 잊지 말자.

Disabling the Functionality

이 기능을 비활성화해야 한다면 프로퍼티에 stubrunner.stream.enabled=false를 설정해라.

Examples

다음과 같이 메이븐 레포지토리에 streamService 애플리케이션에 대한 스텁stub을 배포했다고 가정해보자:

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── streamService
                            ├── 0.0.1-SNAPSHOT
                            │   ├── streamService-0.0.1-SNAPSHOT.pom
                            │   ├── streamService-0.0.1-SNAPSHOT-stubs.jar
                            │   └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

그리고 스텁stub은 다음과 같은 구조를 가지고 있다고 가정한다:

├── META-INF
│   └── MANIFEST.MF
└── repository
    ├── accurest
    │   └── bookReturned1.groovy
    └── mappings

이제 아래 명세contract를 살펴보자:

Contract.make {
	label 'return_book_1'
	input { triggeredBy('bookReturnedTriggered()') }
	outputMessage {
		sentTo('returnBook')
		body('''{ "bookName" : "foo" }''')
		headers { header('BOOK-NAME', 'foo') }
	}
}

다음 Spring Cloud Stream function 설정도 함께 살펴보자:

@ImportAutoConfiguration(TestChannelBinderConfiguration.class)
@Configuration(proxyBeanMethods = true)
@EnableAutoConfiguration
protected static class Config {

	@Bean
	Function<String, String> test1() {
		return (input) -> {
			println "Test 1 [${input}]"
			return input
		}
	}

}

그리고 스프링 설정은 다음과 같다:

stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
stubrunner.stubs-mode: remote
spring:
  cloud:
    stream:
      bindings:
        test1-in-0:
          destination: returnBook
        test1-out-0:
          destination: outputToAssertBook
    function:
      definition: test1

server:
  port: 0

debug: true

return_book_1 레이블을 가진 메시지를 트리거하려면 StubTrigger 인터페이스를 다음과 같이 호출한다:

stubFinder.trigger('return_book_1')

이렇게 하면 명세contract의 출력 메시지에 정의한 목적지로 메시지를 전송한다.

3.5.8. Consumer Side Messaging With Spring JMS

Spring Cloud Contract Stub Runner의 메시지 처리 모듈은 Spring JMS와 쉽게 통합할 수 있는 방법을 제공한다.

Spring JMS와 통합할 때에는, 실행 중인 JMS 브로커 인스턴스가 있다고 가정한다.

Adding the Runner to the Project

클래스패스에 Spring JMS와 Spring Cloud Contract Stub Runner를 모두 추가하면 된다. 테스트 클래스에 @AutoConfigureStubRunner를 선언하는 것을 잊지 말자.

Examples

스텁stub은 다음과 같은 구조를 가지고 있다고 가정한다:

├── stubs
    └── bookReturned1.groovy

그리고 다음은 테스트 설정이다:

stubrunner:
  repository-root: stubs:classpath:/stubs/
  ids: my:stubs
  stubs-mode: remote
spring:
  activemq:
    send-timeout: 1000
  jms:
    template:
      receive-timeout: 1000

이제 아래 명세contract를 살펴보자:

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOKNAME', 'foo')
		}
	}
}

return_book_1 레이블을 가진 메시지를 트리거하려면 StubTrigger 인터페이스를 다음과 같이 호출한다:

stubFinder.trigger('return_book_1')

이렇게 하면 명세contract의 출력 메시지에 정의한 목적지로 메시지를 전송한다.


Next :
3.6. Spring Cloud Contract Stub Runner
Spring Cloud Contract Stub Runner를 사용하는 이유

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

<< >>

TOP