스프링 클라우드 컨트랙트 공식 레퍼런스를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.
Spring Cloud Contract에선 다음과 같은 언어로 DSL을 작성할 수 있다:
- Groovy
- YAML
- Java
- Kotlin
Spring Cloud Contract를 사용하면 하나의 파일에 여러 개의 명세contract를 정의할 수 있다 (Groovy에서는 단일 명세contract가 아닌 리스트를 반환한다).
다음은 명세contract를 정의하는 예시다:
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'PUT'
url '/api/12'
headers {
header 'Content-Type': 'application/vnd.org.springframework.cloud.contract.verifier.twitter-places-analyzer.v1+json'
}
body '''\
[{
"created_at": "Sat Jul 26 09:38:57 +0000 2014",
"id": 492967299297845248,
"id_str": "492967299297845248",
"text": "Gonna see you at Warsaw",
"place":
{
"attributes":{},
"bounding_box":
{
"coordinates":
[[
[-77.119759,38.791645],
[-76.909393,38.791645],
[-76.909393,38.995548],
[-77.119759,38.995548]
]],
"type":"Polygon"
},
"country":"United States",
"country_code":"US",
"full_name":"Washington, DC",
"id":"01fbe706f872cb32",
"name":"Washington",
"place_type":"city",
"url": "https://api.twitter.com/1/geo/id/01fbe706f872cb32.json"
}
}]
'''
}
response {
status OK()
}
}
description: Some description
name: some name
priority: 8
ignored: true
request:
url: /foo
queryParameters:
a: b
b: c
method: PUT
headers:
foo: bar
fooReq: baz
body:
foo: bar
matchers:
body:
- path: $.foo
type: by_regex
value: bar
headers:
- key: foo
regex: bar
response:
status: 200
headers:
foo2: bar
foo3: foo33
fooRes: baz
body:
foo2: bar
foo3: baz
nullValue: null
matchers:
body:
- path: $.foo2
type: by_regex
value: bar
- path: $.foo3
type: by_command
value: executeMe($it)
- path: $.nullValue
type: by_null
value: null
headers:
- key: foo2
regex: bar
- key: foo3
command: andMeToo($it)
import java.util.Collection;
import java.util.Collections;
import java.util.function.Supplier;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.util.ContractVerifierUtil;
class contract_rest implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Collections.singletonList(Contract.make(c -> {
c.description("Some description");
c.name("some name");
c.priority(8);
c.ignored();
c.request(r -> {
r.url("/foo", u -> {
u.queryParameters(q -> {
q.parameter("a", "b");
q.parameter("b", "c");
});
});
r.method(r.PUT());
r.headers(h -> {
h.header("foo", r.value(r.client(r.regex("bar")), r.server("bar")));
h.header("fooReq", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo", "bar"));
r.bodyMatchers(m -> {
m.jsonPath("$.foo", m.byRegex("bar"));
});
});
c.response(r -> {
r.fixedDelayMilliseconds(1000);
r.status(r.OK());
r.headers(h -> {
h.header("foo2", r.value(r.server(r.regex("bar")), r.client("bar")));
h.header("foo3", r.value(r.server(r.execute("andMeToo($it)")), r.client("foo33")));
h.header("fooRes", "baz");
});
r.body(ContractVerifierUtil.map().entry("foo2", "bar").entry("foo3", "baz").entry("nullValue", null));
r.bodyMatchers(m -> {
m.jsonPath("$.foo2", m.byRegex("bar"));
m.jsonPath("$.foo3", m.byCommand("executeMe($it)"));
m.jsonPath("$.nullValue", m.byNull());
});
});
}));
}
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
import org.springframework.cloud.contract.spec.withQueryParameters
contract {
name = "some name"
description = "Some description"
priority = 8
ignored = true
request {
url = url("/foo") withQueryParameters {
parameter("a", "b")
parameter("b", "c")
}
method = PUT
headers {
header("foo", value(client(regex("bar")), server("bar")))
header("fooReq", "baz")
}
body = body(mapOf("foo" to "bar"))
bodyMatchers {
jsonPath("$.foo", byRegex("bar"))
}
}
response {
delay = fixedMilliseconds(1000)
status = OK
headers {
header("foo2", value(server(regex("bar")), client("bar")))
header("foo3", value(server(execute("andMeToo(\$it)")), client("foo33")))
header("fooRes", "baz")
}
body = body(mapOf(
"foo" to "bar",
"foo3" to "baz",
"nullValue" to null
))
bodyMatchers {
jsonPath("$.foo2", byRegex("bar"))
jsonPath("$.foo3", byCommand("executeMe(\$it)"))
jsonPath("$.nullValue", byNull)
}
}
}
메이븐에서는 다음 명령어를 사용하면 명세contract를 스텁stub 매핑 정보로 컴파일할 수 있다:
mvn org.springframework.cloud:spring-cloud-contract-maven-plugin:convert
목차
- 3.1.1. Groovy로 작성하는 Contract DSL
- 3.1.2. Java로 작성하는 Contract DSL
- 3.1.3. Kotlin으로 작성하는 Contract DSL
- 3.1.4. YAML로 작성하는 Contract DSL
- 3.1.5. 제약
- 3.1.6 파일 하나에 Contract 여러 개 정의하기
- 3.1.7. 상태를 가진stateful Contract
3.1.1. Contract DSL in Groovy
Groovy에 익숙하지 않더라도 걱정할 필요 없다. Groovy DSL 파일에서도 Java 문법을 그대로 사용할 수 있다.
Groovy로 명세contract를 작성하기로 결정했다면, Groovy를 사용해 본 적이 없더라도 염려하지 않아도 된다. Contract DSL에선 리터럴, 메소드 호출, 클로저 등 극히 일부 문법만 사용하기 때문에, 언어에 대한 지식은 딱히 필요하지 않다. 게다가 DSL에선 정적인 타입을 사용하기 때문에, DSL 자체에 대한 지식이 전혀 없더라도 개발자라면 충분히 이해할 수 있는 수준이다.
Groovy 명세contract 파일 안에서
Contract
를 사용할 땐, 클래스의 풀 네임fully qualified name을 제공하고org.springframework.cloud.spec.Contract.make { … }
와 같이make
메소드를 static 임포트를 해야 한다는 점을 기억해두자. 아니면Contract
클래스를 임포트한 다음 (import org.springframework.cloud.spec.Contract
),Contract.make { … }
를 호출해도 좋다.
3.1.2. Contract DSL in Java
Java로 명세contract를 정의할 때는, 단일 명세contract는 Supplier<Contract>
인터페이스를, 여러 개의 명세contract는 Supplier<Collection<Contract>>
를 구현한 클래스를 만들어야 한다.
src/test/java
밑에 명세contract를 정의하면 (예를 들면 src/test/java/contracts
), 프로젝트의 클래스패스를 수정하지 않아도 된다. 이 경우 Spring Cloud Contract 플러그인에 명세contract를 정의할 위치를 새로 지정해야 한다.
아래 있는 메이븐, 그래들 예시 모두 src/test/java
경로에 명세contract를 정의한다:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<contractsDirectory>src/test/java/contracts</contractsDirectory>
</configuration>
</plugin>
contracts {
contractsDslDir = new File(project.rootDir, "src/test/java/contracts")
}
3.1.3. Contract DSL in Kotlin
코틀린으로 명세contract를 작성하려면 코틀린 스크립트 파일(.kts
)을 따로 만들어야 한다. Java DSL과 마찬가지로 원하는 경로에 명세contract를 추가하면 된다. 기본적으로 메이븐 플러그인은 src/test/resources/contracts
디렉토리에서, Gradle 플러그인은 src/contractTest/resources/contracts
디렉토리에서 명세contract를 찾는다.
그래들 플러그인은 3.0.0 버전부터 마이그레이션을 고려해 레거시 디렉토리인
src/test/resources/contracts
도 함께 조회한다. 빌드 시 이 디렉토리에서 명세contract를 발견하면 warning 로그가 남게된다.
프로젝트의 플러그인 설정에 spring-cloud-contract-spec-kotlin
의존성을 명시해야 한다. 다음은 메이븐과 그래들을 사용한 예시다:
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- some config -->
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-kotlin</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</plugin>
<dependencies>
<!-- Remember to add this for the DSL support in the IDE and on the consumer side -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-spec-kotlin</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
buildscript {
repositories {
// ...
}
dependencies {
classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:$\{scContractVersion}"
}
}
dependencies {
// ...
// Remember to add this for the DSL support in the IDE and on the consumer side
testImplementation "org.springframework.cloud:spring-cloud-contract-spec-kotlin"
// Kotlin versions are very particular down to the patch version. The <kotlin_version> needs to be the same as you have imported for your project.
testImplementation "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:<kotlin_version>"
}
Kotlin Script 파일 안에서
ContractDSL
을 사용할 땐, 클래스의 풀 네임fully qualified name을 제공해야 한다는 점을 기억해두자.contract
함수는 보통org.springframework.cloud.contract.spec.ContractDsl.contract { … }
와 같이 사용한다. 아니면contract
함수를 임포트한 다음 (import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
)contract { … }
를 호출해도 된다.
3.1.4. Contract DSL in YAML
YAML 명세contract의 스키마는 YML 스키마 페이지에서 확인하면 된다.
3.1.5. Limitations
JSON 배열의 사이즈 검증 기능은 아직 실험 단계다. 이 기능을 사용하려면 시스템 프로퍼티
spring.cloud.contract.verifier.assert.size
의 값을true
로 설정해라. 기본값은false
다. 아니면 플러그인 설정에서assertJsonSize
프로퍼티를 수정할 수도 있다.
JSON 구조는 어떤 형태든 될 수 있기 때문에, Groovy DSL에서
GString
의value(consumer(...), producer(...))
표기법을 사용하면 제대로 파싱되지 않을 수 있다. 그렇기 때문에 Groovy Map 표기법을 사용하는 것 이 좋다.
3.1.6. Multiple Contracts in One File
하나의 파일에 여러 개의 명세contract를 정의하는 것도 가능하다. 다음과 같이 작성하면 된다:
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
name("should post a user")
request {
method 'POST'
url('/users/1')
}
response {
status OK()
}
},
Contract.make {
request {
method 'POST'
url('/users/2')
}
response {
status OK()
}
}
]
---
name: should post a user
request:
method: POST
url: /users/1
response:
status: 200
---
request:
method: POST
url: /users/2
response:
status: 200
---
request:
method: POST
url: /users/3
response:
status: 200
class contract implements Supplier<Collection<Contract>> {
@Override
public Collection<Contract> get() {
return Arrays.asList(
Contract.make(c -> {
c.name("should post a user");
// ...
}), Contract.make(c -> {
// ...
}), Contract.make(c -> {
// ...
})
);
}
}
import org.springframework.cloud.contract.spec.ContractDsl.Companion.contract
arrayOf(
contract {
name("should post a user")
// ...
},
contract {
// ...
},
contract {
// ...
}
}
위 예시에는 name
필드를 사용한 명세contract와 그렇지 않은 명세contract가 하나씩 있다. 이때는 두 개의 테스트가 만들어지게 된다:
import com.example.TestBase;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import com.jayway.restassured.response.ResponseOptions;
import org.junit.Test;
import static com.jayway.restassured.module.mockmvc.RestAssuredMockMvc.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.assertj.core.api.Assertions.assertThat;
public class V1Test extends TestBase {
@Test
public void validate_should_post_a_user() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.post("/users/1");
// then:
assertThat(response.statusCode()).isEqualTo(200);
}
@Test
public void validate_withList_1() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.post("/users/2");
// then:
assertThat(response.statusCode()).isEqualTo(200);
}
}
name
필드가 있는 명세contract의 경우 validate_should_post_a_user
라는 테스트 메소드가 만들어진다. name
필드가 없는 쪽은 validate_withList_1
이다. 이는 WithList.groovy
파일의 이름과 명세contract 목록에서의 인덱스에서 따온 이름이다.
마찬가지로, 다음과 같은 스텁stub이 생성된다:
should post a user.json
1_WithList.json
첫 번째 파일명은 명세contract에 있는 name
파라미터에서 따왔다. 두 번째 파일은 명세contract 파일 이름(WithList.groovy
) 앞에 인덱스를 붙인 것을 확인할 수 있다 (파일 내 명세contract 목록에서 인덱스가 1
인 명세contract).
명세contract에 이름을 지정하면 어떤 테스트인지 쉽게 파악할 수 있어 훨씬 더 좋다.
3.1.7. Stateful Contracts
상태 저장 명세stateful contract(시나리오라고도 부른다)는 순서대로 읽어야 하는 명세contract 정의다. 이는 다음과 같은 상황에서 유용할 수 있다:
-
Spring Cloud Contract를 사용해 상태를 가진stateful 애플리케이션을 테스트할 때, 정확하게 정의한 순서대로 명세contract를 호출하고 싶은 경우
명세contract 테스트는 상태가 없는 것stateless이 좋기 때문에, 추천하지는 않는다.
-
동일한 엔드포인트에서 동일한 요청에 대해, 서로 다른 결과를 반환하기를 원하는 경우
상태 저장 명세stateful contract(또는 시나리오)를 만들려면, 네이밍 컨벤션을 지켜야 한다. 즉, 순번 뒤에 언더스코어를 달아줘야 한다. 이는 YAML이든 Groovy든 관계없이 동일하다. 예시는 다음을 참고해라:
my_contracts_dir\
scenario1\
1_login.groovy
2_showCart.groovy
3_logout.groovy
이 구조에서 Spring Cloud Contract Verifier는 scenario1
이라는 WireMock 시나리오를 생성하고 다음 세 단계를 수행한다:
login
을Started
로 마킹한다. (다음을 가리키는…)showCart
를Step1
으로 마킹한다. (다음을 가리키는…)logout
을 (시나리오를 종료하는)Step2
로 마킹한다.
WireMock 시나리오에 대한 자세한 내용은 여기를 참고해라.
Next :
3.1.8. Common Top-Level Elements
Contract DSL 최상위 요소들
전체 목차는 여기에 있습니다.