Skip to content

Commit fa9f351

Browse files
authored
Merge pull request #1884 from schmidthappens/update/add-support-for-spring7
feat: Add support for Spring 7 and Spring Boot 4
2 parents b5ce914 + f0e01e8 commit fa9f351

30 files changed

+1673
-1
lines changed

pact-jvm-server/src/test/groovy/au/com/dius/pact/server/MainSpec.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class MainSpec extends Specification {
6666
def process = invokeApp(true, '--daemon', '--debug', '31311')
6767

6868
when:
69-
process.waitFor(500, TimeUnit.MILLISECONDS)
69+
process.waitFor(2000, TimeUnit.MILLISECONDS)
7070
def result = createMock('31311', pact)
7171

7272
then:

provider/spring7/README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Pact Spring7/Spring Boot4 + JUnit5 Support
2+
3+
This module extends the base [Pact JUnit5 module](/provider/junit5/README.md) (See that for more details) and adds support
4+
for Spring 7 and Spring Boot 4.
5+
6+
**NOTE: This module requires JDK 17+**
7+
8+
## Dependency
9+
The combined library (JUnit5 + Spring7) is available on maven central using:
10+
11+
group-id = au.com.dius.pact.provider
12+
artifact-id = spring7
13+
version-id = 4.5.x
14+
15+
## Usage
16+
For writing Spring Pact verification tests with JUnit 5, there is an JUnit 5 Invocation Context Provider that you can use with
17+
the `@TestTemplate` annotation. This will generate a test for each interaction found for the pact files for the provider.
18+
19+
To use it, add the `@Provider` and `@ExtendWith(SpringExtension.class)` or `@SpringbootTest` and one of the pact source
20+
annotations to your test class (as per a JUnit 5 test), then add a method annotated with `@TestTemplate` and
21+
`@ExtendWith(PactVerificationSpring7Provider.class)` that takes a `PactVerificationContext` parameter. You will need to
22+
call `verifyInteraction()` on the context parameter in your test template method.
23+
24+
For example:
25+
26+
```java
27+
@AutoConfigureMockMvc
28+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
29+
@Provider("Animal Profile Service")
30+
@PactBroker
31+
public class ContractVerificationTest {
32+
33+
@TestTemplate
34+
@ExtendWith(PactVerificationSpring7Provider.class)
35+
void pactVerificationTestTemplate(PactVerificationContext context) {
36+
context.verifyInteraction();
37+
}
38+
39+
}
40+
```
41+
42+
You will now be able to setup all the required properties using the Spring context, e.g. creating an application
43+
YAML file in the test resources:
44+
45+
```yaml
46+
pactbroker:
47+
host: your.broker.host
48+
auth:
49+
username: broker-user
50+
password: broker.password
51+
```
52+
53+
You can also run pact tests against `MockMvc` without need to spin up the whole application context which takes time
54+
and often requires more additional setup (e.g. database). In order to run lightweight tests just use `@WebMvcTest`
55+
from Spring and `Spring7MockMvcTestTarget` as a test target before each test.
56+
57+
For example:
58+
```java
59+
@WebMvcTest
60+
@Provider("myAwesomeService")
61+
@PactBroker
62+
class ContractVerificationTest {
63+
64+
@Autowired
65+
private MockMvc mockMvc;
66+
67+
@TestTemplate
68+
@ExtendWith(PactVerificationSpring7Provider.class)
69+
void pactVerificationTestTemplate(PactVerificationContext context) {
70+
context.verifyInteraction();
71+
}
72+
73+
@BeforeEach
74+
void before(PactVerificationContext context) {
75+
context.setTarget(new Spring7MockMvcTestTarget(mockMvc));
76+
}
77+
}
78+
```
79+
80+
You can also use `Spring7MockMvcTestTarget` for tests without spring context by providing the controllers manually.
81+
82+
For example:
83+
```java
84+
@Provider("myAwesomeService")
85+
@PactFolder("pacts")
86+
class MockMvcTestTargetStandaloneMockMvcTestJava {
87+
88+
@TestTemplate
89+
@ExtendWith(PactVerificationSpring7Provider.class)
90+
void pactVerificationTestTemplate(PactVerificationContext context) {
91+
context.verifyInteraction();
92+
}
93+
94+
@BeforeEach
95+
void before(PactVerificationContext context) {
96+
Spring7MockMvcTestTarget testTarget = new Spring7MockMvcTestTarget();
97+
testTarget.setControllers(new DataResource());
98+
context.setTarget(testTarget);
99+
}
100+
101+
@RestController
102+
static class DataResource {
103+
@GetMapping("/data")
104+
@ResponseStatus(HttpStatus.NO_CONTENT)
105+
void getData(@RequestParam("ticketId") String ticketId) {
106+
}
107+
}
108+
}
109+
```
110+
111+
**Important:** Since `@WebMvcTest` starts only Spring MVC components you can't use `PactVerificationSpring7Provider`
112+
and need to fallback to `PactVerificationInvocationContextProvider`
113+
114+
## Webflux tests
115+
116+
You can test Webflux routing functions using the `WebFluxSpring7Target` target class. The easiest way to do it is to get Spring to
117+
autowire your handler and router into the test and then pass the routing function to the target.
118+
119+
For example:
120+
121+
```java
122+
@Autowired
123+
YourRouter router;
124+
125+
@Autowired
126+
YourHandler handler;
127+
128+
@BeforeEach
129+
void setup(PactVerificationContext context) {
130+
context.setTarget(new WebFluxSpring7Target(router.route(handler)));
131+
}
132+
133+
@TestTemplate
134+
@ExtendWith(PactVerificationSpring7Provider.class)
135+
void pactVerificationTestTemplate(PactVerificationContext context) {
136+
context.verifyInteraction();
137+
}
138+
```
139+
140+
## Modifying requests
141+
142+
As documented in [Pact JUnit5 module](/provider/junit5/README.md#modifying-the-requests-before-they-are-sent), you can
143+
inject a request object to modify the requests made. However, depending on the Pact test target you are using,
144+
you need to use a different class.
145+
146+
| Test Target | Class to use |
147+
|-----------------------------------------------|----------------------------------|
148+
| HttpTarget, HttpsTarget, SpringBootHttpTarget | org.apache.http.HttpRequest |
149+
| Spring7MockMvcTestTarget | MockHttpServletRequestBuilder |
150+
| WebFluxSpring7Target | WebTestClient.RequestHeadersSpec |
151+
152+
# Verifying V4 Pact files that require plugins
153+
154+
Pact files that require plugins can be verified with version 4.3.0+. For details on how plugins work, see the
155+
[Pact plugin project](https://github.com/pact-foundation/pact-plugins).
156+
157+
Each required plugin is defined in the `plugins` section in the Pact metadata in the Pact file. The plugins will be
158+
loaded from the plugin directory. By default, this is `~/.pact/plugins` or the value of the `PACT_PLUGIN_DIR` environment
159+
variable. Each plugin required by the Pact file must be installed there. You will need to follow the installation
160+
instructions for each plugin, but the default is to unpack the plugin into a sub-directory `<plugin-name>-<plugin-version>`
161+
(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
162+
plugin to be able to be loaded.
163+
164+
# Test Analytics
165+
166+
We are tracking anonymous analytics to gather important usage statistics like JVM version
167+
and operating system. To disable tracking, set the 'pact_do_not_track' system property or environment
168+
variable to 'true'.

provider/spring7/build.gradle

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
plugins {
2+
id 'au.com.dius.pact.kotlin-library-conventions'
3+
}
4+
5+
description = 'Provider Spring7/Spring Boot4 + JUnit5 Support'
6+
group = 'au.com.dius.pact.provider'
7+
8+
dependencies {
9+
api project(':provider:junit5')
10+
11+
implementation 'org.springframework:spring-context:7.0.0'
12+
implementation 'org.springframework:spring-test:7.0.0'
13+
implementation 'org.springframework:spring-web:7.0.0'
14+
implementation 'org.springframework:spring-webflux:7.0.0'
15+
implementation 'jakarta.servlet:jakarta.servlet-api:6.1.0'
16+
implementation 'org.hamcrest:hamcrest:3.0'
17+
implementation 'org.apache.commons:commons-lang3'
18+
implementation 'javax.mail:mail:1.5.0-b01'
19+
20+
testImplementation 'org.springframework.boot:spring-boot-starter-test-classic:4.0.0'
21+
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc:4.0.0'
22+
testImplementation 'org.springframework.boot:spring-boot-starter-security-test:4.0.0'
23+
testImplementation 'org.apache.groovy:groovy'
24+
testImplementation 'org.mockito:mockito-core:5.20.0'
25+
testImplementation 'org.yaml:snakeyaml:2.5'
26+
}

provider/spring7/description.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pact-JVM - Provider Spring7/Spring Boot4 + JUnit5 Support
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package au.com.dius.pact.provider.spring.spring7
2+
3+
import au.com.dius.pact.core.model.Interaction
4+
import au.com.dius.pact.core.model.Pact
5+
import au.com.dius.pact.core.model.PactSource
6+
import au.com.dius.pact.provider.junit5.PactVerificationContext
7+
import au.com.dius.pact.provider.junit5.PactVerificationExtension
8+
import org.junit.jupiter.api.extension.ExtensionContext
9+
import org.junit.jupiter.api.extension.ParameterContext
10+
import org.springframework.test.web.reactive.server.WebTestClient
11+
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder
12+
13+
open class PactVerificationSpring7Extension(
14+
pact: Pact,
15+
pactSource: PactSource,
16+
interaction: Interaction,
17+
serviceName: String,
18+
consumerName: String?
19+
) : PactVerificationExtension(pact, pactSource, interaction, serviceName, consumerName) {
20+
constructor(context: PactVerificationExtension) : this(context.pact, context.pactSource, context.interaction,
21+
context.serviceName, context.consumerName)
22+
23+
override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean {
24+
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
25+
val testContext = store.get("interactionContext") as PactVerificationContext
26+
val target = testContext.currentTarget()
27+
return when (parameterContext.parameter.type) {
28+
MockHttpServletRequestBuilder::class.java -> target is Spring7MockMvcTestTarget
29+
WebTestClient.RequestHeadersSpec::class.java -> target is WebFluxSpring7Target
30+
else -> super.supportsParameter(parameterContext, extensionContext)
31+
}
32+
}
33+
34+
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any? {
35+
val store = extensionContext.getStore(ExtensionContext.Namespace.create("pact-jvm"))
36+
return when (parameterContext.parameter.type) {
37+
MockHttpServletRequestBuilder::class.java -> store.get("request")
38+
WebTestClient.RequestHeadersSpec::class.java -> store.get("request")
39+
else -> super.resolveParameter(parameterContext, extensionContext)
40+
}
41+
}
42+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package au.com.dius.pact.provider.spring.spring7
2+
3+
import au.com.dius.pact.core.support.expressions.ValueResolver
4+
import au.com.dius.pact.provider.junit5.PactVerificationExtension
5+
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider
6+
import org.junit.jupiter.api.extension.ExtensionContext
7+
import org.junit.jupiter.api.extension.TestTemplateInvocationContext
8+
import org.springframework.test.context.TestContextManager
9+
import org.springframework.test.context.junit.jupiter.SpringExtension
10+
import java.util.stream.Stream
11+
12+
open class PactVerificationSpring7Provider : PactVerificationInvocationContextProvider() {
13+
14+
override fun getValueResolver(context: ExtensionContext): ValueResolver? {
15+
val store = context.root.getStore(ExtensionContext.Namespace.create(SpringExtension::class.java))
16+
val testClass = context.requiredTestClass
17+
val testContextManager = store.getOrComputeIfAbsent(testClass, { TestContextManager(testClass) },
18+
TestContextManager::class.java)
19+
val environment = testContextManager.testContext.applicationContext.environment
20+
return Spring7EnvironmentResolver(environment)
21+
}
22+
23+
override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream<TestTemplateInvocationContext> {
24+
return super.provideTestTemplateInvocationContexts(context).map {
25+
if (it is PactVerificationExtension) {
26+
PactVerificationSpring7Extension(it)
27+
} else {
28+
it
29+
}
30+
}
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package au.com.dius.pact.provider.spring.spring7
2+
3+
import au.com.dius.pact.core.support.expressions.SystemPropertyResolver
4+
import au.com.dius.pact.core.support.expressions.ValueResolver
5+
import org.springframework.core.env.Environment
6+
7+
class Spring7EnvironmentResolver(private val environment: Environment) : ValueResolver {
8+
override fun resolveValue(property: String?): String? {
9+
val tuple = SystemPropertyResolver.PropertyValueTuple(property).invoke()
10+
11+
val name = tuple.propertyName ?: return null
12+
val defaultValue = tuple.defaultValue ?: return null
13+
14+
return environment.getProperty(name, defaultValue)
15+
}
16+
17+
override fun resolveValue(property: String?, default: String?): String? {
18+
val name = property ?: return null
19+
val defaultValue = default ?: return null
20+
return environment.getProperty(name, defaultValue)
21+
}
22+
23+
override fun propertyDefined(property: String) = environment.containsProperty(property)
24+
}

0 commit comments

Comments
 (0)