스프링 클라우드 컨트랙트 공식 레퍼런스를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.
Spring Cloud Contract를 사용하면 메시지를 통해 통신하는 애플리케이션을 검증할 수 있다. 이 문서에서 언급하는 모든 통합 기능은 스프링을 기반으로 동작하지만, 원한다면 직접 만든 구현체를 사용할 수도 있다.
목차
- 3.5.1. 메시지 처리 DSL 최상위 요소
- 3.5.2. 통합
- 3.5.3. 프로듀서에서 메시지 처리 테스트 코드 생성하기
- 3.5.4. 컨슈머에서 Stub 생성하기
- 3.5.5. Apache Camel로 컨슈머 측 메시지 처리하기
- 3.5.6. Spring Integration으로 컨슈머 측 메시지 처리하기
- 3.5.7. Spring Cloud Stream으로 컨슈머 측 메시지 처리하기
- 3.5.8. Spring JMS로 컨슈머 측 메시지 처리하기
3.5.1. Messaging DSL Top-level Elements
메시지 처리를 위한 DSL은 HTTP 전용 DSL과는 조금 다르게 생겼다. 그 차이점들은 아래 섹션에서 나누어 설명한다:
Output Triggered by a Method
아래 예시에서 처럼, 특정 메소드를 호출해 메시지 출력을 트리거할 수 있다 (명세contract가 시작되는 시점이나, 메시지를 전송한 시점의 Scheduler
등으로):
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는 이 메소드를 호출해 메시지를 트리거하는 테스트를 생성한다. 컨슈머consumer는 some_label
을 사용해 메시지를 트리거할 수 있다.
Consumer/Producer
이 섹션에서 설명하는 내용은 Groovy DSL에만 해당하는 내용이다.
HTTP에선 client
/stub
, server
/test
라는 개념이 존재한다. 메시지를 처리할 때에도 유사한 개념을 사용할 수 있다. 또한 Spring Cloud Contract Verifier는 consumer
와 producer
메소드도 제공한다 ($
또는 value
메소드를 사용해 consumer
와 producer
값을 따로 정의할 수 있다).
Common
input
이나 outputMessage
섹션에서 assertThat
을 호출하려면, 베이스 클래스에 정의한 메소드나 스태틱 임포트로 가져온 메소드를 이용하면 된다 (e.g. assertThatMessageIsOnTheQueueue()
). Spring Cloud Contract가 자동 생성한 테스트에선 이 메소드를 호출한다.
3.5.2. Integrations
통합 설정은 다음 중 하나를 사용할 수 있다:
- Apache Camel
- Spring Integration
- Spring Cloud Stream
- Spring JMS
여기서는 스프링 부트를 사용하고 있기 때문에, 클래스패스에 이 라이브러리 중 하나를 추가하면, 메시지 처리에 필요한 모든 설정이 자동으로 세팅된다.
자동 생성 테스트에서 사용할 베이스 클래스에
@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.MessageVerifierSender
와 org.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를 살펴보자:
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
위 예제에선 다음과 같은 테스트가 만들어진다:
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를 사용하는 이유
전체 목차는 여기에 있습니다.