Design Overview
Question
Protobuf enforces some level of type checking, and a kind of contract, for request/response objects. However, how do I develop, maintain and enforce RESTful contracts (HTTP verb + path + request + response)?
Is this the way to go?
Develop contracts in Spring Cloud Contract and auto-generate integration contract tests.
You can check out a sample that uses protobuffers with spring cloud contract on the producer side here https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/producer_proto and on the consumer side here https://github.com/spring-cloud-samples/spring-cloud-contract-samples/tree/master/consumer_proto
The very idea is to treat the content as binary. Let's say that I stored the request and response in a binary format in a .bin file. Then I can create the following contract
package contracts.beer.rest
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Represents a successful scenario of getting a beer
```
given:
client is old enough
when:
he applies for a beer
then:
we'll grant him the beer
```
""")
request {
method 'POST'
url '/check'
body(fileAsBytes("PersonToCheck_old_enough.bin"))
headers {
contentType("application/x-protobuf")
}
}
response {
status 200
body(fileAsBytes("Response_old_enough.bin"))
headers {
contentType("application/x-protobuf")
}
}
}
Having such a controller
@RestController
public class ProducerController {
private final PersonCheckingService personCheckingService;
public ProducerController(PersonCheckingService personCheckingService) {
this.personCheckingService = personCheckingService;
}
@RequestMapping(value = "/check",
method=RequestMethod.POST,
consumes="application/x-protobuf",
produces="application/x-protobuf")
public Beer.Response check(@RequestBody Beer.PersonToCheck personToCheck) {
//remove::start[]
if (this.personCheckingService.shouldGetBeer(personToCheck)) {
return Beer.Response.newBuilder().setStatus(Beer.Response.BeerCheckStatus.OK).build();
}
return Beer.Response.newBuilder().setStatus(Beer.Response.BeerCheckStatus.NOT_OK).build();
//remove::end[return]
}
}
interface PersonCheckingService {
Boolean shouldGetBeer(Beer.PersonToCheck personToCheck);
}
and such a base class for the generated tests (I assume that you've setup the contract plugin)
package com.example;
//remove::start[]
import io.restassured.module.mockmvc.RestAssuredMockMvc;
//remove::end[]
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.WebApplicationContext;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = BeerRestBase.Config.class)
public abstract class BeerRestBase {
@Autowired
WebApplicationContext context;
//remove::start[]
@Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
// remove::end[]
@Configuration
@EnableAutoConfiguration
@Import({ ProtoConfiguration.class, ProducerController.class })
static class Config {
@Bean
PersonCheckingService personCheckingService() {
return argument -> argument.getAge() >= 20;
}
}
}
will result in proper test and stub generation. Check out the aforementioned sample for concrete implementation details.
On the consumer side you can fetch the stubs and run your tests against them
package com.example;
import org.assertj.core.api.BDDAssertions;
import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.contract.stubrunner.junit.StubRunnerRule;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
/**
* @author Marcin Grzejszczak
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class ProtoTest {
@Autowired
RestTemplate restTemplate;
int port;
@Rule
public StubRunnerRule rule = new StubRunnerRule()
.downloadStub("com.example", "beer-api-producer-proto")
.stubsMode(StubRunnerProperties.StubsMode.LOCAL);
@Before
public void setupPort() {
this.port = this.rule.findStubUrl("beer-api-producer-proto").getPort();
}
@Test
public void should_give_me_a_beer_when_im_old_enough() throws Exception {
Beer.Response response = this.restTemplate.postForObject(
"http://localhost:" + this.port + "/check",
Beer.PersonToCheck.newBuilder().setAge(23).build(), Beer.Response.class);
BDDAssertions.then(response.getStatus()).isEqualTo(Beer.Response.BeerCheckStatus.OK);
}
@Test
public void should_reject_a_beer_when_im_too_young() throws Exception {
Beer.Response response = this.restTemplate.postForObject(
"http://localhost:" + this.port + "/check",
Beer.PersonToCheck.newBuilder().setAge(17).build(), Beer.Response.class);
response = response == null ? Beer.Response.newBuilder().build() : response;
BDDAssertions.then(response.getStatus()).isEqualTo(Beer.Response.BeerCheckStatus.NOT_OK);
}
}
Again, please check the concrete sample for implementation details.