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

au.com.dius.pact.provider.spring.SpringInteractionRunner.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.Interaction
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.core.model.UnknownPactSource
import au.com.dius.pact.provider.junit.InteractionRunner
import au.com.dius.pact.provider.junit.target.Target
import au.com.dius.pact.provider.spring.target.SpringBootHttpTarget
import org.junit.After
import org.junit.Before
import org.junit.runners.model.FrameworkMethod
import org.junit.runners.model.MultipleFailureException
import org.junit.runners.model.Statement
import org.junit.runners.model.TestClass
import org.springframework.test.context.TestContextManager
import java.lang.reflect.Method

open class SpringBeforeRunner(
  private val next: Statement,
  private val befores: List,
  private val testInstance: Any,
  private val testMethod: Method,
  private val testContextManager: TestContextManager
) : Statement() {

  override fun evaluate() {
    testContextManager.beforeTestMethod(testInstance, testMethod)
    for (before in befores) {
      before.invokeExplosively(testInstance)
    }
    next.evaluate()
  }
}

open class SpringAfterRunner(
  private val next: Statement,
  private val afters: List,
  private val testInstance: Any,
  private val testMethod: Method,
  private val testContextManager: TestContextManager
) : Statement() {

  override fun evaluate() {
    val errors: MutableList = mutableListOf()
    var testException: Throwable? = null
    try {
      next.evaluate()
    } catch (e: Throwable) {
      testException = e
      errors.add(e)
    } finally {
      for (each in afters) {
        try {
          each.invokeExplosively(testInstance)
        } catch (e: Throwable) {
          errors.add(e)
        }
      }
    }

    try {
      testContextManager.afterTestMethod(testInstance, testMethod, testException)
    } catch (ex: Throwable) {
      errors.add(ex)
    }

    MultipleFailureException.assertEmpty(errors)
  }
}

open class SpringInteractionRunner(
  testClass: TestClass,
  pact: Pact,
  pactSource: PactSource?,
  private val testContextManager: TestContextManager
) : InteractionRunner(testClass, pact, pactSource ?: UnknownPactSource) where I : Interaction {

  override fun withBefores(interaction: Interaction, testInstance: Any, statement: Statement): Statement {
    val befores = testClass.getAnnotatedMethods(Before::class.java)
    return SpringBeforeRunner(statement, befores, testInstance,
      this.javaClass.getMethod("surrogateTestMethod"), testContextManager)
  }

  override fun withAfters(interaction: Interaction, testInstance: Any, statement: Statement): Statement {
    val afters = testClass.getAnnotatedMethods(After::class.java)
    return SpringAfterRunner(statement, afters, testInstance,
      this.javaClass.getMethod("surrogateTestMethod"), testContextManager)
  }

  override fun createTest(): Any {
    val test = super.createTest()
    testContextManager.prepareTestInstance(test)
    return test
  }

  override fun setupTargetForInteraction(target: Target) {
    super.setupTargetForInteraction(target)

    if (target is SpringBootHttpTarget) {
      val environment = testContextManager.testContext.applicationContext.environment
      val port = environment.getProperty("local.server.port")
      target.port = Integer.parseInt(port)
    }
  }
}