Search code examples
javapactpact-jvmpact-java

Using java to create PACT I am not able to set the min value of the numberType in the body


I am learning how to use PACT into my Java project and I would like to define some values restrictions on the expected output. For example, into one request /hello-world I am expecting to receive a number into the id attribute that should be always bigger than zero.

package com.thiagomata.pact.hello.consumer.consumer;

import au.com.dius.pact.consumer.ConsumerPactBuilder;
import au.com.dius.pact.consumer.PactVerificationResult;
import static io.pactfoundation.consumer.dsl.LambdaDsl.newJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.model.MockProviderConfig;
import au.com.dius.pact.model.RequestResponsePact;
import com.thiagomata.pact.hello.consumer.models.Greeting;
import io.pactfoundation.consumer.dsl.LambdaDslJsonBody;
import org.junit.Assert;
import org.junit.Test;
import scala.tools.jline_embedded.internal.Log;

import static au.com.dius.pact.consumer.ConsumerPactRunnerKt.runConsumerTest;
import static org.junit.Assert.assertEquals;

public class NameApplicationPactTest {

    @Test
    public void testNamePact() throws Throwable {

        Log.debug("inside the test");
        /**
         * Creating the mock server
         *
         * Define the expected input
         *  Using relative address
         *  The provider address will be automatically created
         *  The provider port will be automatically created
         * Define the expected output
         *  Keep the id as a undefined integer
         *  Set the content to the test
         */
        RequestResponsePact pact = ConsumerPactBuilder
            .consumer("hello_world_consumer")
            .hasPactWith("hello_world_provider")
            .uponReceiving("a request of hello world")
            .path("/hello-world")
            .matchQuery("name","johny")
            .method("GET")
            .willRespondWith()
            .body(
                newJsonBody( (LambdaDslJsonBody o) -> o.
                    numberType("id"). // <====================================
                    stringType("content", "Hello johny")
                ).build()
            )
            .toPact();

        /**
         * Let the Pact define the mock server address and port
         */
        MockProviderConfig config = MockProviderConfig.createDefault();

        /**
         * Create the mock server into the defined config and with the
         * pact result prepared
         */
        PactVerificationResult result = runConsumerTest(
            pact,
            config,
            mockServer -> {

                Log.debug("inside mock server");
                /**
                 * Pass the mock server configuration to the consumer classes
                 */
                DummyConsumer consumer = new DummyConsumer(
                    mockServer.getUrl(),
                    mockServer.getPort(),
                    "johny"
                );

                /**
                 * Now, when the code internally fires to the
                 * mockServer we should get the expected answer
                 */
                Greeting greeting = consumer.getGreeting();

                Log.debug(greeting);

                Assert.assertNotNull(
                    "Greeting id should not be null",
                    greeting.getId()
                );

                /**
                 * Currently I am not able to define a rule into the
                 * DSL Matching methods to assure that the value should
                 * be bigger than 0
                 */
                Assert.assertTrue( greeting.getId() > 0 ); // <=================================================

                assertEquals(
                    "Validate expected default greeting content",
                    "Hello johny",
                    greeting.getContent()
                );

                Log.debug("status code = " + consumer.getStatusCode() );

                Assert.assertTrue(
                    "test consumer status code",
                    consumer.getStatusCode().equals(
                        200
                    )
                );
            }
        );

        /**
         * If some Assert inside of the anonymous functions fails
         * it will not automatically throw a failure.
         *
         * We need to capture the error from the result
         */
        if (result instanceof PactVerificationResult.Error) {
            throw ((PactVerificationResult.Error) result).getError();
        }

        assertEquals(PactVerificationResult.Ok.INSTANCE, result);
    }
}

Someone could say that PACT it is not able to apply such restrictions. But, looking to the generated PACT, it looks like that creating min value and max value to the generators should be a possible into the PACT:

    {
      "provider": {
        "name": "hello_world_provider"
      },
      "consumer": {
        "name": "hello_world_consumer"
      },
      "interactions": [
        {
          "description": "Test User Service",
          "request": {
            "method": "GET",
            "path": "/hello-world"
          },
          "response": {
            "status": 200,
            "headers": {
              "content-type": "application/json",
              "Content-Type": "application/json; charset\u003dUTF-8"
            },
            "body": {
              "id": 100,
              "content": "string"
            },
            "matchingRules": {
              "body": {
                "$.id": {
                  "matchers": [
                    {
                      "match": "integer"
                    }
                  ],
                  "combine": "AND"
                },
                "$.content": {
                  "matchers": [
                    {
                      "match": "type"
                    }
                  ],
                  "combine": "AND"
                }
              }
            },
            "generators": {
              "body": {
                "$.id": {
                  "type": "RandomInt",
                  "min": 0, /* <======================================== */
                  "max": 2147483647
                },
                "$.content": {
                  "type": "RandomString",
                  "size": 20
                }
              }
            }
          },
          "providerStates": [
            {
              "name": "default"
            }
          ]
        }
      ],
      "metadata": {
        "pact-specification": {
          "version": "3.0.0"
        },
        "pact-jvm": {
          "version": "3.5.10"
        }
      }
    }

I try to find some way to do so, looking to the PACT code. So, following the track of the numberType method, of the LambdaDsl:

/* ... */

public LambdaDslObject numberType(final String... names) {
    object.numberType(names);
    return this;
}

/* ... */

That method calls the object.numberTypes that it is into the LambdaDslJsonBody with this possible methods:

/**
 * Attribute that can be any number
 * @param name attribute name
 */
public PactDslJsonBody numberType(String name) {
    generators.addGenerator(
        Category.BODY, 
        matcherKey(name), 
        new RandomIntGenerator(0, Integer.MAX_VALUE) // <========================
    );
    return numberType(name, 100);
}

/**
 * Attributes that can be any number
 * @param names attribute names
 */
public PactDslJsonBody numberType(String... names) {
  for (String name: names) {
    numberType(name);
  }
  return this;
}

/**
 * Attribute that can be any number
 * @param name attribute name
 * @param number example number to use for generated bodies
 */
public PactDslJsonBody numberType(String name, Number number) {
    body.put(name, number);
    matchers.addRule(matcherKey(name), new NumberTypeMatcher(NumberTypeMatcher.NumberType.NUMBER));
    return this;
}

Where there is only one generator, that starts always with zero.

So, there is some possible way to create this kind of random generator to PACT that assures that the value of the generated random number will be bigger than zero, or less than 100?


Solution

  • It is doable (replacing default value generator), but it requires applying a small workaround. You can add a custom generator to generators list returned by DslPart.getGenerators() method, something like:

    DslPart.getGenerators()
                .addGenerator(Category.BODY, ".id", new RandomIntGenerator(0, 100));
    

    It will override generator for $.id field created when calling .numberType("id") method. Take a look at this exemplary consumer contract test:

    import au.com.dius.pact.consumer.Pact;
    import au.com.dius.pact.consumer.PactProviderRuleMk2;
    import au.com.dius.pact.consumer.PactVerification;
    import au.com.dius.pact.consumer.dsl.DslPart;
    import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
    import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
    import au.com.dius.pact.model.RequestResponsePact;
    import au.com.dius.pact.model.generators.Category;
    import au.com.dius.pact.model.generators.RandomIntGenerator;
    import org.codehaus.jackson.map.ObjectMapper;
    import org.junit.Rule;
    import org.junit.Test;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    import java.util.HashMap;
    import java.util.Map;
    
    import static org.hamcrest.CoreMatchers.equalTo;
    import static org.hamcrest.CoreMatchers.is;
    import static org.hamcrest.MatcherAssert.assertThat;
    
    public class PactIntGeneratorTest {
    
        @Rule
        public PactProviderRuleMk2 mockProvider = new PactProviderRuleMk2("providerA", "localhost", 8080, this);
    
        @Pact(consumer = "consumerA", provider = "providerA")
        public RequestResponsePact requestA(PactDslWithProvider builder) throws Exception {
            final DslPart body = new PactDslJsonBody()
                    .numberType("id")
                    .stringType("content", "Hello johny");
    
            body.getGenerators()
                    .addGenerator(Category.BODY, ".id", new RandomIntGenerator(0, 100));
    
            return builder
                    .uponReceiving("(GET) /foo")
                        .path("/foo")
                        .method("GET")
                    .willRespondWith()
                        .status(200)
                        .body(body)
                    .toPact();
        }
    
        @Test
        @PactVerification(fragment = "requestA")
        public void testRequestA() throws IOException, InterruptedException {
            //given:
            final ObjectMapper objectMapper = new ObjectMapper();
    
            //when:
            final InputStream json = new URL("http://localhost:8080/foo").openConnection().getInputStream();
            final Map response = objectMapper.readValue(json, HashMap.class);
    
            //then:
            assertThat(((Integer) response.get("id")) > 0, is(true));
            //and:
            assertThat(response.get("content"), is(equalTo("Hello johny")));
        }
    }
    

    It's not exact your case, but it shows how to override generator for $.id field. After running this test following Pact file gets generated:

    {
        "provider": {
            "name": "providerA"
        },
        "consumer": {
            "name": "consumerA"
        },
        "interactions": [
            {
                "description": "(GET) /foo",
                "request": {
                    "method": "GET",
                    "path": "/foo"
                },
                "response": {
                    "status": 200,
                    "headers": {
                        "Content-Type": "application/json; charset=UTF-8"
                    },
                    "body": {
                        "id": 100,
                        "content": "Hello johny"
                    },
                    "matchingRules": {
                        "body": {
                            "$.id": {
                                "matchers": [
                                    {
                                        "match": "number"
                                    }
                                ],
                                "combine": "AND"
                            },
                            "$.content": {
                                "matchers": [
                                    {
                                        "match": "type"
                                    }
                                ],
                                "combine": "AND"
                            }
                        }
                    },
                    "generators": {
                        "body": {
                            "$.id": {
                                "type": "RandomInt",
                                "min": 0,
                                "max": 100
                            }
                        }
                    }
                }
            }
        ],
        "metadata": {
            "pact-specification": {
                "version": "3.0.0"
            },
            "pact-jvm": {
                "version": "3.5.10"
            }
        }
    }
    

    As you can see RandomIntGenerator is used with attributes min:0 and max:100.

    Last thing worth mentioning: the difference between contract tests and functional tests

    Keep in mind that generators are only used for generating values that are passed to a provider when provider's contract verification test is running. The custom generator I've created does not modify the contract - it doesn't say that only values between 0 and 100 are correct one. It will only generate a value in this range when provider executes contract verification. So your contract still is valid for id like 1001 or 12700 etc. And this is fine, because contract tests are not functional tests. Consumers should not force these kind of business rules. Otherwise you could quickly run into situation where consumerA says that id is correct when it's between 0 and 100, while consumerB says that id is correct only if it's between 99 and 999. I know it's tempting to create very concrete contracts, but it's a straight way to over-specification that holds provider from development. Hope it helps.