Reading:
Injecting values from provider states

Injecting values from provider states

Ronald Holshausen
Updated

One of the challengers we face when verifying our Pacts against a provider is the situation where we have no control over some of the data the provider returns. Typical examples of this is auto-generated values from the database.

The mechanism we use to setup validation test data is called Provider States. Essentially, these define the state the provider needs to be in to be able to successfully verify a particular interaction from the pact file. They are normally implemented as a callback that gets executed before the actual test is executed and are good for setting up test fixture data.

But in the case of a restful web service where the resource IDs are generated by the database, we may have no idea of knowing what the actual resource ID is before the provider state callback is executed. And as it is common with REST to include the IDs in the URL for a resource, this makes it hard to implement the verification test without annoying work arounds.

In this post I'm going to tackle this problem using the Generators feature of the V3 Pact specification. These are mechanisms in which values can be generated during the test execution, and we are going to use one that was recently added to Pact-JVM: the provider state generator.

You will be able to find all the code examples I've used for this post at pactflow/injected-provider-states-example.

Enter the Problem

We have to implement two restful web services were the first is dependant on the second (and we'll use Springboot for this example).

The first service (we will call it the Consumer in the examples) is a transaction service. It is responsible for managing some type of transaction against an account. We will deal with the first requirement of the transaction service: creating a new transaction.

The second service (called the Provider in the examples) is the account service. It is responsible for all the account details. And to make our example a bit more complex (of course this never happens in real life!), each account has two auto-generated values that we have little control over: the account ID and number.

So for the case where the transaction service gets a request to create a new transaction against an account, it will have to call out to the account service using the account number from the transaction data to get all the account details before creating the transaction.

Easy enough to do with Spring boot.

Consumer Pact Test

The consumer pact test is going to be easy to write. There is a transaction controller with a method that gets called to handle the new transaction request. We can just call that method.

Here is the Pact test:

@ExtendWith(PactConsumerTestExt::class)
@PactTestFor(providerName = "AccountService")
@SpringBootTest
class TransactionPactTest {

  @Autowired
  private lateinit var controller: TransactionController

  @Pact(consumer = "TransactionService")
  fun accounts(builder: PactDslWithProvider) = builder
      .given("Account Test001 exists", "accountRef", "Test001") //(1)
      .uponReceiving("a request to get the account details")
      .path("/accounts/search/findOneByAccountNumberId")
      .query("accountNumber=100") //(2)
      .willRespondWith()
      .status(200)
      .headers(mapOf("Content-Type" to "application/hal+json"))
      .body(
        PactDslJsonBody()
          .integerType("id", 1)
          .integerType("version", 0)
          .stringType("name", "Test")
          .stringValue("accountRef", "Test001")
          .timestamp("createdDate")
          .timestamp("lastModifiedDate")
          .`object`("accountNumber")
            .integerType("id", 100) //(3)
          .closeObject()
          .`object`("_links")
            .`object`("self")
              .matchUrl("href", "http://localhost:8080", "/accounts", "\\d+")
            .closeObject()
            .`object`("account")
              .matchUrl("href", "http://localhost:8080", "/accounts", "\\d+")
            .closeObject()
          .closeObject()
      ).toPact()

  @Test
  @PactTestFor(pactMethod = "accounts")
  fun testNewTransaction(mockServer: MockServer) {
    controller.providerUrl = mockServer.getUrl()
    val result = controller.new(100)
    assertThat(result, hasKey("account"))
    val account = result["account"] as Account
    assertThat(account.accountNumber?.id, `is`(equalTo(100)))
  }
}
TransactionPactTest.kt

We have setup a provider state with the given method in the DSL (marked as (1)) in the example above. This will allow a provide state callback to be executed that can create the account entries in the database before the test is validated on the provider side.

However, we had to use a made up value for the account number in our test (100). The actual value will be different, and we have no way of knowing what it will be beforehand. This is be okay for most things, as we have used matchers so that actual value won't be a problem (see (3)). But the problem is that we need to use that account number as part of the URL for the resource that will be fetched (see (2)). We can't use just any value here, otherwise we will get a 404 response back.

Using a value from the provider state

To solve this problem we can inject the value from the provider state callback. The callback can return a map of key value pairs, and we can reference those in our test. That way we can use the actual account number of the account that the provider state callback will create.

This works by using a generator that can expand an expression with values substituted from the provider state callback. Let's change the test to do this. We only need to change two lines.

.query("accountNumber=100")

changes to

.queryParameterFromProviderState("accountNumber", "\${accountNumber}", "100")

and

.`object`("accountNumber")
  .integerType("id", 100)
.closeObject()

changes to

.`object`("accountNumber")
  .valueFromProviderState("id", "\${accountNumber}", 100)
.closeObject()

Now when we run our test, everything still works the same using the example value of 100, but the difference will be when we validate the provider.

Validating the provider

We can now write a provider verification test. It would be quite simple, with the main thing we need to ensure is that our provider state method returns the correct value.

@Provider("AccountService")
@PactFolder("pacts")
@SpringBootTest
class PactVerificationTest {

  @Autowired
  lateinit var acountRepository: AcountRepository

  @TestTemplate
  @ExtendWith(PactVerificationInvocationContextProvider::class)
  fun testTemplate(context: PactVerificationContext) {
    context.verifyInteraction()
  }

  @State("Account Test001 exists")
  fun createAccount(params: Map<String, String>): Map<String, Any> {
    val account = Account(0, 0, params["accountRef"]!!,
      AccountNumber(0), params["accountRef"]!!)
    val persistedAccount = acountRepository.save(account)
    return mapOf("accountNumber" to persistedAccount.accountNumber.id)
  }

}
PactVerificationTest.kt

This test is doing a few things. It is starting the application with the @SpringBootTest annotation. It is auto-wiring in the AcountRepository which we need for creating the account in the database. It is running all interactions against the running application defined by pacts found by @PactFolder("pacts") that match @Provider("AccountService").

The magic bit is the createAccount provider state callback. This is executed before the actual interaction from the pact file. It receives the parameters from the provider state (which we specified in the consumer pact test using the given method). It then creates the account in the database for the test using the account repository. And lastly, it returns the auto-generated account number from the database.

In Conclusion

Generators are a great way of dealing with data that can not be persisted in the contract because they are dynamic in nature. I have now shown how we can use this mechanism to deal with auto-generated values from a database that can be injected into the contract verification at runtime.

Other types of dynamic data, are things like authorisation tokens, dates and times. My next post is going to be on dealing with services that require dates relative to some period (like financial year end).

arrow-up icon