Skip to content

Commit

Permalink
feat: add support for Spring 6 and Springboot 3 #1660
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Feb 14, 2023
1 parent 6e1ff7f commit bffd26a
Show file tree
Hide file tree
Showing 29 changed files with 1,605 additions and 2 deletions.
167 changes: 167 additions & 0 deletions provider/spring6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Pact Spring6/Springboot3 + JUnit5 Support

This module extends the base [Pact JUnit5 module](/provider/junit5/README.md) (See that for more details) and adds support
for Spring 6 and Springboot 3.

**NOTE: This module requires JDK 17+**

## Dependency
The combined library (JUnit5 + Spring6) is available on maven central using:

group-id = au.com.dius.pact.provider
artifact-id = spring6
version-id = 4.5.x

## Usage
For writing Spring Pact verification tests with JUnit 5, there is an JUnit 5 Invocation Context Provider that you can use with
the `@TestTemplate` annotation. This will generate a test for each interaction found for the pact files for the provider.

To use it, add the `@Provider` and `@ExtendWith(SpringExtension.class)` or `@SpringbootTest` and one of the pact source
annotations to your test class (as per a JUnit 5 test), then add a method annotated with `@TestTemplate` and
`@ExtendWith(PactVerificationSpring6Provider.class)` that takes a `PactVerificationContext` parameter. You will need to
call `verifyInteraction()` on the context parameter in your test template method.

For example:

```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Provider("Animal Profile Service")
@PactBroker
public class ContractVerificationTest {

@TestTemplate
@ExtendWith(PactVerificationSpring6Provider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}

}
```

You will now be able to setup all the required properties using the Spring context, e.g. creating an application
YAML file in the test resources:

```yaml
pactbroker:
host: your.broker.host
auth:
username: broker-user
password: broker.password
```

You can also run pact tests against `MockMvc` without need to spin up the whole application context which takes time
and often requires more additional setup (e.g. database). In order to run lightweight tests just use `@WebMvcTest`
from Spring and `Spring6MockMvcTestTarget` as a test target before each test.

For example:
```java
@WebMvcTest
@Provider("myAwesomeService")
@PactBroker
class ContractVerificationTest {

@Autowired
private MockMvc mockMvc;

@TestTemplate
@ExtendWith(PactVerificationSpring6Provider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}

@BeforeEach
void before(PactVerificationContext context) {
context.setTarget(new Spring6MockMvcTestTarget(mockMvc));
}
}
```

You can also use `Spring6MockMvcTestTarget` for tests without spring context by providing the controllers manually.

For example:
```java
@Provider("myAwesomeService")
@PactFolder("pacts")
class MockMvcTestTargetStandaloneMockMvcTestJava {

@TestTemplate
@ExtendWith(PactVerificationSpring6Provider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}

@BeforeEach
void before(PactVerificationContext context) {
Spring6MockMvcTestTarget testTarget = new Spring6MockMvcTestTarget();
testTarget.setControllers(new DataResource());
context.setTarget(testTarget);
}

@RestController
static class DataResource {
@GetMapping("/data")
@ResponseStatus(HttpStatus.NO_CONTENT)
void getData(@RequestParam("ticketId") String ticketId) {
}
}
}
```

**Important:** Since `@WebMvcTest` starts only Spring MVC components you can't use `PactVerificationSpring6Provider`
and need to fallback to `PactVerificationInvocationContextProvider`

## Webflux tests

You can test Webflux routing functions using the `WebFluxSpring6Target` target class. The easiest way to do it is to get Spring to
autowire your handler and router into the test and then pass the routing function to the target.

For example:

```java
@Autowired
YourRouter router;

@Autowired
YourHandler handler;

@BeforeEach
void setup(PactVerificationContext context) {
context.setTarget(new WebFluxSpring6Target(router.route(handler)));
}

@TestTemplate
@ExtendWith(PactVerificationSpring6Provider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
```

## Modifying requests

As documented in [Pact JUnit5 module](/provider/junit5/README.md#modifying-the-requests-before-they-are-sent), you can
inject a request object to modify the requests made. However, depending on the Pact test target you are using,
you need to use a different class.

| Test Target | Class to use |
|-----------------------------------------------|----------------------------------|
| HttpTarget, HttpsTarget, SpringBootHttpTarget | org.apache.http.HttpRequest |
| Spring6MockMvcTestTarget | MockHttpServletRequestBuilder |
| WebFluxSpring6Target | WebTestClient.RequestHeadersSpec |

# Verifying V4 Pact files that require plugins

Pact files that require plugins can be verified with version 4.3.0+. For details on how plugins work, see the
[Pact plugin project](https://github.com/pact-foundation/pact-plugins).

Each required plugin is defined in the `plugins` section in the Pact metadata in the Pact file. The plugins will be
loaded from the plugin directory. By default, this is `~/.pact/plugins` or the value of the `PACT_PLUGIN_DIR` environment
variable. Each plugin required by the Pact file must be installed there. You will need to follow the installation
instructions for each plugin, but the default is to unpack the plugin into a sub-directory `<plugin-name>-<plugin-version>`
(i.e., for the Protobuf plugin 0.0.0 it will be `protobuf-0.0.0`). The plugin manifest file must be present for the
plugin to be able to be loaded.

# Test Analytics

We are tracking anonymous analytics to gather important usage statistics like JVM version
and operating system. To disable tracking, set the 'pact_do_not_track' system property or environment
variable to 'true'.
35 changes: 35 additions & 0 deletions provider/spring6/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
plugins {
id 'au.com.dius.pact.kotlin-library-conventions'
}

description = 'Provider Spring6/Springboot3 + JUnit5 Support'
group = 'au.com.dius.pact.provider'

java {
targetCompatibility = '17'
sourceCompatibility = '17'
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "17"
}
}

dependencies {
api project(':provider:junit5')

implementation 'org.springframework:spring-context:6.0.4'
implementation 'org.springframework:spring-test:6.0.4'
implementation 'org.springframework:spring-web:6.0.4'
implementation 'org.springframework:spring-webflux:6.0.4'
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
implementation 'org.hamcrest:hamcrest:2.2'
implementation 'org.apache.commons:commons-lang3'
implementation 'javax.mail:mail:1.5.0-b01'

testImplementation 'org.springframework.boot:spring-boot-starter-test:3.0.2'
testImplementation 'org.springframework.boot:spring-boot-starter-web:3.0.2'
testImplementation 'org.apache.groovy:groovy'
testImplementation 'org.mockito:mockito-core:4.8.1'
}
1 change: 1 addition & 0 deletions provider/spring6/description.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pact-JVM - Provider Spring6/Springboot3 + JUnit5 Support
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package au.com.dius.pact.provider.spring.spring6

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.provider.junit5.PactVerificationContext
import au.com.dius.pact.provider.junit5.PactVerificationExtension
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder

open class PactVerificationSpring6Extension(
pact: Pact,
pactSource: PactSource,
interaction: Interaction,
serviceName: String,
consumerName: String?
) : PactVerificationExtension(pact, pactSource, interaction, serviceName, consumerName) {
constructor(context: PactVerificationExtension) : this(context.pact, context.pactSource, context.interaction,
context.serviceName, context.consumerName)

override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
val testContext = store.get("interactionContext") as PactVerificationContext
return when (parameterContext.parameter.type) {
MockHttpServletRequestBuilder::class.java -> testContext.target is Spring6MockMvcTestTarget
WebTestClient.RequestHeadersSpec::class.java -> testContext.target is WebFluxSpring6Target
else -> super.supportsParameter(parameterContext, extensionContext)
}
}

override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any? {
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
return when (parameterContext.parameter.type) {
MockHttpServletRequestBuilder::class.java -> store.get("request")
WebTestClient.RequestHeadersSpec::class.java -> store.get("request")
else -> super.resolveParameter(parameterContext, extensionContext)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package au.com.dius.pact.provider.spring.spring6

import au.com.dius.pact.core.support.expressions.ValueResolver
import au.com.dius.pact.provider.junit5.PactVerificationExtension
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.TestTemplateInvocationContext
import org.springframework.test.context.TestContextManager
import org.springframework.test.context.junit.jupiter.SpringExtension
import java.util.stream.Stream

open class PactVerificationSpring6Provider : PactVerificationInvocationContextProvider() {

override fun getValueResolver(context: ExtensionContext): ValueResolver? {
val store = context.root.getStore(ExtensionContext.Namespace.create(SpringExtension::class.java))
val testClass = context.requiredTestClass
val testContextManager = store.getOrComputeIfAbsent(testClass, { TestContextManager(testClass) },
TestContextManager::class.java)
val environment = testContextManager.testContext.applicationContext.environment
return Spring6EnvironmentResolver(environment)
}

override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream<TestTemplateInvocationContext> {
return super.provideTestTemplateInvocationContexts(context).map {
if (it is PactVerificationExtension) {
PactVerificationSpring6Extension(it)
} else {
it
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package au.com.dius.pact.provider.spring.spring6

import au.com.dius.pact.core.support.expressions.SystemPropertyResolver
import au.com.dius.pact.core.support.expressions.ValueResolver
import org.springframework.core.env.Environment

class Spring6EnvironmentResolver(private val environment: Environment) : ValueResolver {
override fun resolveValue(property: String?): String? {
val tuple = SystemPropertyResolver.PropertyValueTuple(property).invoke()
return environment.getProperty(tuple.propertyName, tuple.defaultValue)
}

override fun resolveValue(property: String?, default: String?): String? {
return environment.getProperty(property, default)
}

override fun propertyDefined(property: String) = environment.containsProperty(property)
}

0 comments on commit bffd26a

Please sign in to comment.