Consumer

Driven

Contracts

Jędrzej Andrykowski

JUG Zielona Góra 21.12.2016

Które
testy będą odpowiednie?
Dlaczego
Consumer-Driven Contracts?
Jak wykorzystać
Spring Cloud Contract Verifier?

Które testy będą odpowiednie?

Request

Response

Client

Server

Jak przetestować?

Testy

jednostkowe

Testy akceptacyjne

?

Co z komunikacją?
Dlaczego powinniśmy ją testować?

Request

Response

Client

Server

Client

Server

POST /users

1.0.0

1.2.3

201
POST /users
201

Client

Server

POST /users

1.1.0

1.2.3

POST /users
400

Może End2End?

Ale...

Czasochłonne

Ciężkie

Kruche

Blokujące

Powiązane

Mało wiarygodne

Można lepiej...

                                           Dlaczego

Consumer-Driven

              Contracts?

...zgodne porozumienie dwóch lub więcej stron ustalające ich wzajemne prawa lub obowiązki...

Kontrakt

Client

Server

POST /users

1.0.0

1.2.3

201
POST /users
201

IF

THEN

Kontrakt pomiędzy klientem a serwerem

1.2.3

1.0.0

1.1.0

1.0.0

1.1.0

Kontakt ustalony przez serwer

Kontrakt sterowany potrzebami konsumenta

1.2.3

1.2.3

1.2.3

Czy to oznacza, że serwer nie ma już nic do gadania???

Kontrakty są tematami do rozmów i ustaleń

Sukces serwera jest uzależniony od jego poprawnego konsumowania

Implementacja

Arkusz lub dokument

Klient pisze testy serwerowi

PULL

PUSH

Jak to połączyć?

Jak wykorzystać

Jak wykorzystać

Spring Cloud

Contract Verifier

Contract

PUSH

STUBS

REPOSITORY

Stubs
(JSON)

Server Tests

Spring Cloud

Contract Verifier

Wiremock

Stubs

   Runs server tests

 

 

PULL

Client

Server

GENERATE

GENERATE

   Runs client tests

 

 

PUSH

PULL

Serwer

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'POST'
        urlPath('/users')
        headers {
            header 'Content-Type': 'application/json'
        }
        body(
                name: $(client(regex('[a-zA-Z]+')), server('Jan')),
        )
    }

    response {
        status 201
        body(
                id: $(client('123'), server(regex('\\d+'))),
                name: $(client('Jan'), server(regex('[a-zA-Z]+')))
        )
        headers {
            header 'Content-Type': 'application/json'
        }
    }
}

Definiujemy kontrakt

class ServerSpec extends Specification {

    UserService usersServiceMock = Stub(UserService) {
        createUser(_) >> new User(123, "Jan")
    }

    def setup() {
        def userController = new UserRestController(usersServiceMock)
        RestAssuredMockMvc.standaloneSetup(userController)
    }
}

Definiujemy bazową klasę testów

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:1.0.2.RELEASE"
    }
}

...

contracts {
    targetFramework = 'Spock'
    contractsDslDir = file('./cdc-contracts')
    generatedTestSourcesDir = file('./build/generated-test-sources')
    baseClassForTests = 'pl.jug.cdc.server.ServerSpec'
    basePackageForTests = 'pl.jug.cdc.server.contracts'
}

Podstawowa konfiguracja - Gradle

./gradlew clean build
class ContractVerifierSpec extends ServerSpec {

	def validate_createNewUser() throws Exception {
	    given:
                def request = given().header("Content-Type", "application/json")
			             .body('''{"name":"Jan"}''')
            when:
		def response = given().spec(request)
				      .post("/users")

            then:
		response.statusCode == 201
		response.header('Content-Type')  == 'application/json'
            and:
		DocumentContext parsedJson = JsonPath.parse(response.body.asString())
		assertThatJson(parsedJson).field("id").matches("\\d+")
		assertThatJson(parsedJson).field("name").matches("[a-zA-Z]+")
	}

}

Wygenerowany test dla serwera

{
  "uuid" : "c2cef9bb-e1f0-4bb6-aaf2-e02c034fcc47",
  "request" : {
    "urlPath" : "/users",
    "method" : "POST",
    "headers" : {
      "Content-Type" : {
        "equalTo" : "application/json"
      }
    },
    "bodyPatterns" : [ {
      "matchesJsonPath" : "$[?(@.name =~ /[a-zA-Z]+/)]"
    } ]
  },
  "response" : {
    "status" : 201,
    "body" : "{\"id\":\"123\",\"name\":\"Jan\"}",
    "headers" : {
      "Content-Type" : "application/json"
    }
  }
}

Wygenerowany stub dla klienta

Klient

dependencies {
    testCompile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner:1.0.2.RELEASE'
}

Podstawowa konfiguracja - Gradle

@RunWith(SpringRunner.class)
@DirtiesContext
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureStubRunner( workOffline = true, ids = {"pl.jug.cdc:server:+:stubs:8090"})
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @Test
    public void shouldCreateNewUser() {
        //given

        //when
        final UserResponse result = userService.createNewUser("Jan");

        //then
        assertThat(result.getId()).isNotNull();
        assertThat(result.getName()).isNotBlank();
    }
}

Test klienta

Nowości

  • common jar;
  • documentation generator ;
  • custom contract converter;
  • custom stub generator;
  • custom test generator;

Więcej o CDC...

  • Consumer Driven Contracts
    http://martinfowler.com/articles/consumerDrivenContracts.html
  • Spring Cloud Contract Verifier
    https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html
  • Technology Radar
    https://www.thoughtworks.com/radar/techniques/consumer-driven-contract-testing
  • Kod z prezentacji
    https://github.com/zielona-gora-jug/consumer-driven-contracts