All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.com.dius.pact.provider.spring.MvcProviderVerifier.kt Maven / Gradle / Ivy

Go to download

# Pact Spring/JUnit runner ## Overview Library provides ability to play contract tests against a provider using Spring & JUnit. This library is based on and references the JUnit package, so see the [Pact JUnit 4](../pact-jvm-provider-junit) or [Pact JUnit 5](../pact-jvm-provider-junit5) providers for more details regarding configuration using JUnit. Supports: - Standard ways to load pacts from folders and broker - Easy way to change assertion strategy - Spring Test MockMVC Controllers and ControllerAdvice using MockMvc standalone setup. - MockMvc debugger output - Multiple @State runs to test a particular Provider State multiple times - **au.com.dius.pact.provider.junit.State** custom annotation - before each interaction that requires a state change, all methods annotated by `@State` with appropriate the state listed will be invoked. **NOTE:** For publishing provider verification results to a pact broker, make sure the Java system property `pact.provider.version` is set with the version of your provider. ## Example of MockMvc test ```java @RunWith(RestPactRunner.class) // Custom pact runner, child of PactRunner which runs only REST tests @Provider("myAwesomeService") // Set up name of tested provider @PactFolder("pacts") // Point where to find pacts (See also section Pacts source in documentation) public class ContractTest { //Create an instance of your controller. We cannot autowire this as we're not using (and don't want to use) a Spring test runner. @InjectMocks private AwesomeController awesomeController = new AwesomeController(); //Mock your service logic class. We'll use this to create scenarios for respective provider states. @Mock private AwesomeBusinessLogic awesomeBusinessLogic; //Create an instance of your controller advice (if you have one). This will be passed to the MockMvcTarget constructor to be wired up with MockMvc. @InjectMocks private AwesomeControllerAdvice awesomeControllerAdvice = new AwesomeControllerAdvice(); //Create a new instance of the MockMvcTarget and annotate it as the TestTarget for PactRunner @TestTarget public final MockMvcTarget target = new MockMvcTarget(); @Before //Method will be run before each test of interaction public void before() { //initialize your mocks using your mocking framework MockitoAnnotations.initMocks(this); //configure the MockMvcTarget with your controller and controller advice target.setControllers(awesomeController); target.setControllerAdvice(awesomeControllerAdvice); } @State("default", "no-data") // Method will be run before testing interactions that require "default" or "no-data" state public void toDefaultState() { target.setRunTimes(3); //let's loop through this state a few times for a 3 data variants when(awesomeBusinessLogic.getById(any(UUID.class))) .thenReturn(myTestHelper.generateRandomReturnData(UUID.randomUUID(), ExampleEnum.ONE)) .thenReturn(myTestHelper.generateRandomReturnData(UUID.randomUUID(), ExampleEnum.TWO)) .thenReturn(myTestHelper.generateRandomReturnData(UUID.randomUUID(), ExampleEnum.THREE)); } @State("error-case") public void SingleUploadExistsState_Success() { target.setRunTimes(1); //tell the runner to only loop one time for this state //you might want to throw exceptions to be picked off by your controller advice when(awesomeBusinessLogic.getById(any(UUID.class))) .then(i -> { throw new NotCoolException(i.getArgumentAt(0, UUID.class).toString()); }); } } ``` ## Using Spring runners You can use `SpringRestPactRunner` or `SpringMessagePactRunner` instead of the default Pact runner to use the Spring test annotations. This will allow you to inject or mock spring beans. `SpringRestPactRunner` is for restful webapps and `SpringMessagePactRunner` is for async message tests. For example: ```java @RunWith(SpringRestPactRunner.class) @Provider("pricing") @PactBroker(protocol = "https", host = "${pactBrokerHost}", port = "443", authentication = @PactBrokerAuth(username = "${pactBrokerUser}", password = "${pactBrokerPassword}")) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) public class PricingServiceProviderPactTest { @MockBean private ProductClient productClient; // This will replace the bean with a mock in the application context @TestTarget @SuppressWarnings(value = "VisibilityModifier") public final Target target = new HttpTarget(8091); @State("Product X010000021 exists") public void setupProductX010000021() throws IOException { reset(productClient); ProductBuilder product = new ProductBuilder() .withProductCode("X010000021"); when(productClient.fetch((Set<String>) argThat(contains("X010000021")), any())).thenReturn(product); } @State("the product code X00001 can be priced") public void theProductCodeX00001CanBePriced() throws IOException { reset(productClient); ProductBuilder product = new ProductBuilder() .withProductCode("X00001"); when(productClient.find((Set<String>) argThat(contains("X00001")), any())).thenReturn(product); } } ``` ### Using Spring Context Properties The SpringRestPactRunner will look up any annotation expressions (like `${pactBrokerHost}`) above) from the Spring context. For Springboot, this will allow you to define the properties in the application test properties. For instance, if you create the following `application.yml` in the test resources: ```yaml pactbroker: host: "your.broker.local" port: "443" protocol: "https" auth: username: "<your broker username>" password: "<your broker password>" ``` Then you can use the defaults on the `@PactBroker` annotation. ```java @RunWith(SpringRestPactRunner.class) @Provider("My Service") @PactBroker( authentication = @PactBrokerAuth(username = "${pactbroker.auth.username}", password = "${pactbroker.auth.password}") ) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class PactVerificationTest { ``` ### Using a random port with a Springboot test If you use a random port in a springboot test (by setting `SpringBootTest.WebEnvironment.RANDOM_PORT`), you need to set it to the `TestTarget`. How this works is different for JUnit4 and JUnit5. #### JUnit4 You can use the `SpringBootHttpTarget` which will get the application port from the spring application context. For example: ```java @RunWith(SpringRestPactRunner.class) @Provider("My Service") @PactBroker @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class PactVerificationTest { @TestTarget public final Target target = new SpringBootHttpTarget(); } ``` #### JUnit5 You actually don't need to dependend on `pact-jvm-provider-spring` for this. It's sufficient to depend on `pact-jvm-provider-junit5`. You can set the port to the `HttpTestTarget` object in the before method. ```java @Provider("My Service") @PactBroker @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class PactVerificationTest { @LocalServerPort private int port; @BeforeEach void before(PactVerificationContext context) { context.setTarget(new HttpTestTarget("localhost", port)); } } ```

The newest version!
package au.com.dius.pact.provider.spring

import au.com.dius.pact.core.model.Request
import au.com.dius.pact.core.model.RequestResponseInteraction
import au.com.dius.pact.provider.ProviderClient
import au.com.dius.pact.provider.ProviderInfo
import au.com.dius.pact.provider.ProviderVerifier
import groovy.lang.Binding
import groovy.lang.Closure
import groovy.lang.GroovyShell
import mu.KLogging
import org.apache.commons.lang3.StringUtils
import org.hamcrest.Matchers.anything
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.MediaType
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.mock.web.MockMultipartFile
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.RequestBuilder
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.request
import org.springframework.web.util.UriComponentsBuilder
import scala.Function1
import java.net.URI
import java.util.concurrent.Callable
import java.util.function.Consumer
import java.util.function.Function
import javax.mail.internet.ContentDisposition
import javax.mail.internet.MimeMultipart
import javax.mail.util.ByteArrayDataSource

/**
 * Verifies the providers against the defined consumers using Spring MockMvc
 */
open class MvcProviderVerifier(private val debugRequestResponse: Boolean = false) : ProviderVerifier() {

  fun verifyResponseFromProvider(
    provider: ProviderInfo,
    interaction: RequestResponseInteraction,
    interactionMessage: String,
    failures: MutableMap,
    mockMvc: MockMvc
  ) {
    try {
      val request = interaction.request

      val mvcResult = executeMockMvcRequest(mockMvc, request, provider)

      val expectedResponse = interaction.response
      val actualResponse = handleResponse(mvcResult.response)

      verifyRequestResponsePact(expectedResponse, actualResponse, interactionMessage, failures,
        interaction.interactionId.orEmpty())
    } catch (e: Exception) {
      failures[interactionMessage] = e
      reporters.forEach {
        it.requestFailed(provider, interaction, interactionMessage, e, projectHasProperty.apply(PACT_SHOW_STACKTRACE))
      }
    }
  }

  fun executeMockMvcRequest(mockMvc: MockMvc, request: Request, provider: ProviderInfo): MvcResult {
    val body = request.body
    val requestBuilder = if (body != null && body.isPresent()) {
      if (request.isMultipartFileUpload()) {
        val multipart = MimeMultipart(ByteArrayDataSource(body.unwrap(), request.contentTypeHeader()))
        val multipartRequest = MockMvcRequestBuilders.fileUpload(requestUriString(request))
        var i = 0
        while (i < multipart.count) {
          val bodyPart = multipart.getBodyPart(i)
          val contentDisposition = ContentDisposition(bodyPart.getHeader("Content-Disposition").first())
          val name = StringUtils.defaultString(contentDisposition.getParameter("name"), "file")
          val filename = contentDisposition.getParameter("filename").orEmpty()
          multipartRequest.file(MockMultipartFile(name, filename, bodyPart.contentType, bodyPart.inputStream))
          i++
        }
        multipartRequest.headers(mapHeaders(request, true))
      } else {
        MockMvcRequestBuilders.request(HttpMethod.valueOf(request.method), requestUriString(request))
          .headers(mapHeaders(request, true))
          .content(body.value)
      }
    } else {
      MockMvcRequestBuilders.request(HttpMethod.valueOf(request.method), requestUriString(request))
        .headers(mapHeaders(request, false))
    }

    executeRequestFilter(requestBuilder, provider)

    return performRequest(mockMvc, requestBuilder).andDo {
      if (debugRequestResponse) {
        MockMvcResultHandlers.print().handle(it)
      }
    }.andReturn()
  }

  private fun executeRequestFilter(requestBuilder: MockHttpServletRequestBuilder, provider: ProviderInfo) {
    val requestFilter = provider.requestFilter
    if (requestFilter != null) {
      when (requestFilter) {
        is Closure<*> -> requestFilter.call(requestBuilder)
        is Function1<*, *> -> (requestFilter as Function1).apply(requestBuilder)
        is org.apache.commons.collections4.Closure<*> ->
          (requestFilter as org.apache.commons.collections4.Closure).execute(requestBuilder)
        else -> {
          if (ProviderClient.isFunctionalInterface(requestFilter)) {
            invokeJavaFunctionalInterface(requestFilter, requestBuilder)
          } else {
            val binding = Binding()
            binding.setVariable(ProviderClient.REQUEST, requestBuilder)
            val shell = GroovyShell(binding)
            shell.evaluate(requestFilter as String)
          }
        }
      }
    }
  }

  private fun invokeJavaFunctionalInterface(functionalInterface: Any, requestBuilder: MockHttpServletRequestBuilder) {
    when (functionalInterface) {
      is Consumer<*> -> (functionalInterface as Consumer).accept(requestBuilder)
      is Function<*, *> -> (functionalInterface as Function).apply(requestBuilder)
      is Callable<*> -> (functionalInterface as Callable).call()
      else -> throw IllegalArgumentException("Java request filters must be either a Consumer or Function that " +
        "takes at least one MockHttpServletRequestBuilder parameter")
    }
  }

  private fun performRequest(mockMvc: MockMvc, requestBuilder: RequestBuilder): ResultActions {
    val resultActions = mockMvc.perform(requestBuilder)
    return if (resultActions.andReturn().request.isAsyncStarted) {
      mockMvc.perform(asyncDispatch(resultActions
        .andExpect(request().asyncResult(anything()))
        .andReturn()))
    } else {
      resultActions
    }
  }

  fun requestUriString(request: Request): URI {
    val uriBuilder = UriComponentsBuilder.fromPath(request.path)

    val query = request.query
    if (query != null && query.isNotEmpty()) {
      query.forEach { key, value ->
        uriBuilder.queryParam(key, *value.toTypedArray())
      }
    }

    return URI.create(uriBuilder.toUriString())
  }

  fun mapHeaders(request: Request, hasBody: Boolean): HttpHeaders {
    val httpHeaders = HttpHeaders()

    request.headers?.forEach { k, v ->
      httpHeaders.add(k, v.joinToString(", "))
    }

    if (hasBody && !httpHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
      httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    }

    return httpHeaders
  }

  fun handleResponse(httpResponse: MockHttpServletResponse): Map {
    logger.debug { "Received response: ${httpResponse.status}" }
    val response = mutableMapOf("statusCode" to httpResponse.status)

    val headers = mutableMapOf>()
    httpResponse.headerNames.forEach { headerName ->
      headers[headerName] = listOf(httpResponse.getHeader(headerName))
    }
    response["headers"] = headers

    if (httpResponse.contentType.isNullOrEmpty()) {
      response["contentType"] = org.apache.http.entity.ContentType.APPLICATION_JSON
    } else {
      response["contentType"] = org.apache.http.entity.ContentType.parse(httpResponse.contentType.toString())
    }
    response["data"] = httpResponse.contentAsString

    logger.debug { "Response: $response" }

    return response
  }

  companion object : KLogging()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy