remondis-it / pact-consumer-builder

Library for generating pact consumer expectations from java beans
Apache License 2.0
14 stars 6 forks source link

Maven Central JCenter Build Status

Table of Contents

  1. Long Story Short
  2. How to use
    1. Custom global data type mappings
    2. Global Java Bean mappings
    3. Declare field mappings
    4. Top-level collections
    5. Root value objects
  3. Pacts from Spring Pageable and Sort
  4. How to contribute

Long Story Short

This library tries to reduce the overhead of writing the expectations of JSON structures in PACT consumer test. Normally you have to specify each field with its type and a sample value separately on PactDslJsonBody. If the backend uses Java Beans as a representation for JSON structures, this library performs the necessary calls to the PACT consumer API. This avoids boilerplate code. Changes to your Java Bean model will be automatically reflected by the PACT consumer tests.

The following example shows what is necessary for a PACT consumer test. Assume you want to declare an object holding pricing information, like currency, amount etc. The following PACT consumer test would need the following code:

    PactDslJsonBody jsonBody = new PactDslJsonBody().object("currency")
        .numberType("id", expectedPricingResult.getCurrency()
            .getId())
        .stringType("name", expectedPricingResult.getCurrency()
            .getName())
        .stringType("isoCode", expectedPricingResult.getCurrency()
            .getIsoCode())
        .stringType("symbol", expectedPricingResult.getCurrency()
            .getSymbol())
        .closeObject()
        .object("total")
        // and more fields...

This mapping between the Java Bean and the JSON structure can be generated by this library. All you need to do is to declare what data types are to be mapped. For primitive types a default mapping comes out-of-the-box. For special requirements and custom data types you can specify individual mapping functions on a type or field basis.

The mapping code for the above structure reduces to

ConsumerExpects.type(PricingResultResource.class)
    .useTypeMapping(ZonedDateTime.class, (body, fieldName, fieldValue) -> {
      return body.stringType(fieldName, DEFAULT_FORMATTER.format(fieldValue));
    })
    .build(jsonBody, expectedPricingResult);

The above code shows that all primitive types can be automatically translated to respective calls on PactDslJsonBody to declare the JSON structure. The full example uses a field with type ZonedDateTime, so a custom converter for this type is added in the example.

You can find the full example here

How to use

This library converts Java Bean properties in respective calls on the PactDslJsonBody API. Here are the data type mappings that are active by default:

Data type Pact DSL Mapping
String pactDslJsonBody.stringType(fieldName, fieldValue);
byte/Byte pactDslJsonBody.numberType(fieldName, fieldValue);
short/Short pactDslJsonBody.numberType(fieldName, fieldValue);
int/Integer pactDslJsonBody.integerType(fieldName, fieldValue);
long/Long pactDslJsonBody.integerType(fieldName, fieldValue);
float/Float pactDslJsonBody.numberType(fieldName, fieldValue);
double/Double pactDslJsonBody.decimalType(fieldName, (Double) fieldValue);
boolean/Boolean pactDslJsonBody.booleanType(fieldName, fieldValue);
BigDecimal pactDslJsonBody.decimalType(fieldName, (BigDecimal) fieldValue);

Custom global data type mappings

If you want to map objects that does not comply to the Java Bean convention you can add a custom mapping in the following way:

ConsumerExpects.type(SomeJavaBeanType.class)
    .useTypeMapping(NonJavaBeanType.class, (body, fieldName, fieldValue) -> {
      return body.stringType(fieldName, asString(fieldValue));
    })

The example shows how to add a mapping for the non-Java Bean type NonJavaBeanType. You can specify a function that takes the PactDslJsonBody, the field name and an example value for the field and invokes the respective methods on the PactDslJsonBody instance. The function must return the resulting PactDslJsonBody instance.

Global data type mappings apply to all fields of this type. You can override global mappings with field mappings.

Global Java Bean mappings

You can reuse Java Bean mappings you declared using this library. If Java Bean references another Java Bean that was already mapped using this library, you can reuse the mapping like this:

ConsumerBuilder<Address> addressDefinition =
    ConsumerExpects.type(Address.class)
    // ... other mapping definitions...
    ;

ConsumerExpects.type(Person.class)
.referencing(addressDefinition)
// ... other mapping definitions...

If type Person references Address and the Address structure was already defined, you can reuse the Address as a reference from Person. When building the JSON structure, the Address will be mapped as defined in the registered ConsumerBuilder.

Declare field mappings

Field mappings are used to override global mappings on a per-field basis or to introduce special cases. You can basically

Top-level collections

To create a PACT consumer test for an endpoint that returns a collection as the top level element, use the special API entry point:

PactDslJsonArray pactDslJsonArray = ConsumerExpects.collectionOf(<LIST_ITEM_ELEMENT_TYPE>.class)
   .useArraySupplier(supplier) // Use a custom array structure supplier.
   .build(<LIST_ITEM_SAMPLE_HERE>);

For a complete example please refer to this example.

Root value objects

The PACT Dsl provides a way to define root values. Root values are values that can be represented by a simple string. When defining arrays using the PACT Dsl, root values must be declared using au.com.dius.pact.consumer.dsl.PactDslJsonRootValue instead of using the methods that are used for fields within complex objects.

To support root values, this library provides a second interface com.remondis.cdc.consumer.pactbuilder.PactDslRootValueModifier<T>. This interface provides the known methods of PactDslModifier but also contains a mandatory method to return the specific PactDslJsonRootValue.

If you define data types, that should be rendered as simple strings, always use PactDslRootValueModifier when declaring a custom modifier.

Here is an example of a custom modifier definition for an object that should be represented as a simple string:

public class IntegerMapping implements PactDslRootValueModifier<Integer> {

  // Define the PACT Dsl Json Body - relevant when used as a field within a complex object.
  @Override
  public PactDslJsonBody apply(PactDslJsonBody pactDslJsonBody, String fieldName, Integer fieldValue) {
    return pactDslJsonBody.integerType(fieldName, fieldValue);
  }

  // Define the PACT Dsl Json Root Value - relevant when used as a simple value within an array.
  @Override
  public PactDslJsonRootValue asRootValue(Integer fieldValue) {
    return PactDslJsonRootValue.integerType(fieldValue);
  }

}

Pacts from Spring Pageable and Sort

Pact consumer bodies can be build using the types PageBean provided by this library. The original data types Page, PageImpl and Sort cannot be used due to missing default constructors.

The following example shows how to build a pact consumer body for a Page using the JavaBean-versions:

  @Test
  public void shuldGeneratePactFromPage() {
    PageBean<Dto> pageBean = new PageBean<>(asList(new Dto("forename1", "name1"), new Dto("forename2", "name2")));
    ConsumerExpects.type(PageBean.class)
        .useTypeMapping(Sort.class, SpringSortModifier.sortModifier())
        .field(PageBean::getContent)
        .as(ConsumerExpects.type(Dto.class))
        .build(new PactDslJsonBody(), pageBean);
  }

The class com.remondis.cdc.consumer.pactbuilder.external.springsupport.SpringSortModifier provides a custom JSON body definition for the class org.springframework.data.domain.Sort. If you use this custom definition a sort structure will be defined in the resulting pact, but it uses sample data. So the sort values of your PageBean object will not be reflected in the resulting pact.

How to contribute

Please refer to the project's contribution guide