au.com.dius.pact.provider.junit5.TestTarget.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pact-jvm-provider-junit5 Show documentation
Show all versions of pact-jvm-provider-junit5 Show documentation
# Pact Junit 5 Extension
## Overview
For writing 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 one of the pact source annotations to your test class (as per a JUnit 4 test), then
add a method annotated with `@TestTemplate` and `@ExtendWith(PactVerificationInvocationContextProvider.class)` that
takes a `PactVerificationContext` parameter. You will need to call `verifyInteraction()` on the context parameter in
your test template method.
For example:
```java
@Provider("myAwesomeService")
@PactFolder("pacts")
public class ContractVerificationTest {
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
}
```
For details on the provider and pact source annotations, refer to the [Pact junit runner](../pact-jvm-provider-junit/README.md) docs.
## Test target
You can set the test target (the object that defines the target of the test, which should point to your provider) on the
`PactVerificationContext`, but you need to do this in a before test method (annotated with `@BeforeEach`). There are three
different test targets you can use: `HttpTestTarget`, `HttpsTestTarget` and `AmpqTestTarget`.
For example:
```java
@BeforeEach
void before(PactVerificationContext context) {
context.setTarget(HttpTestTarget.fromUrl(new URL(myProviderUrl)));
// or something like
// context.setTarget(new HttpTestTarget("localhost", myProviderPort, "/"));
}
```
**Note for Maven users:** If you use Maven to run your tests, you will have to make sure that the Maven Surefire plugin is at least
version 2.22.1 uses an isolated classpath.
For example, configure it by adding the following to your POM:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
```
## Provider State Methods
Provider State Methods work in the same way as with JUnit 4 tests, refer to the [Pact junit runner](../pact-jvm-provider-junit/README.md) docs.
### Using multiple classes for the state change methods
If you have a large number of state change methods, you can split things up by moving them to other classes. You will
need to specify the additional classes on the test context in a `Before` method. Do this with the `withStateHandler`
or `setStateHandlers` methods. See [StateAnnotationsOnAdditionalClassTest](src/test/java/au/com/dius/pact/provider/junit5/StateAnnotationsOnAdditionalClassTest.java) for an example.
## Modifying the requests before they are sent
**Important Note:** You should only use this feature for things that can not be persisted in the pact file. By modifying
the request, you are potentially modifying the contract from the consumer tests!
Sometimes you may need to add things to the requests that can't be persisted in a pact file. Examples of these would be
authentication tokens, which have a small life span. The Http and Https test targets support injecting the request that
will executed into the test template method.
You can then add things to the request before calling the `verifyInteraction()` method.
For example to add a header:
```java
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void testTemplate(PactVerificationContext context, HttpRequest request) {
// This will add a header to the request
request.addHeader("X-Auth-Token", "1234");
context.verifyInteraction();
}
```
## Objects that can be injected into the test methods
You can inject the following objects into your test methods (just like the `PactVerificationContext`). They will be null if injected before the
supported phase.
| Object | Can be injected from phase | Description |
| ------ | --------------- | ----------- |
| PactVerificationContext | @BeforeEach | The context to use to execute the interaction test |
| Pact | any | The Pact model for the test |
| Interaction | any | The Interaction model for the test |
| HttpRequest | @TestTemplate | The request that is going to be executed (only for HTTP and HTTPS targets) |
| ProviderVerifier | @TestTemplate | The verifier instance that is used to verify the interaction |
## Allowing the test to pass when no pacts are found to verify (version 4.0.7+)
By default, the test will fail with an exception if no pacts were found to verify. This can be overridden by adding the
`@IgnoreNoPactsToVerify` annotation to the test class. For this to work, you test class will need to be able to receive
null values for any of the injected parameters.
The newest version!
package au.com.dius.pact.provider.junit5
import au.com.dius.pact.core.model.DirectorySource
import au.com.dius.pact.core.model.Interaction
import au.com.dius.pact.core.model.PactBrokerSource
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.core.model.RequestResponseInteraction
import au.com.dius.pact.core.model.messaging.Message
import au.com.dius.pact.provider.ConsumerInfo
import au.com.dius.pact.provider.HttpClientFactory
import au.com.dius.pact.provider.IProviderVerifier
import au.com.dius.pact.provider.PactVerification
import au.com.dius.pact.provider.ProviderClient
import au.com.dius.pact.provider.ProviderInfo
import org.apache.http.client.methods.HttpUriRequest
import java.net.URL
import java.net.URLClassLoader
import java.util.function.Function
import java.util.function.Supplier
/**
* Interface to a test target
*/
interface TestTarget {
/**
* Returns information about the provider
*/
fun getProviderInfo(serviceName: String, pactSource: PactSource? = null): ProviderInfo
/**
* Prepares the request for the interaction.
*
* @return a pair of the client class and request to use for the test, or null if there is none
*/
fun prepareRequest(interaction: Interaction, context: Map): Pair?
/**
* If this is a request response (HTTP or HTTPS) target
*/
fun isHttpTarget(): Boolean
/**
* Executes the test (using the client and request from prepareRequest, if any)
*
* @return Map of failures, or an empty map if there were not any
*/
fun executeInteraction(client: Any?, request: Any?): Map
/**
* Prepares the verifier for use during the test
*/
fun prepareVerifier(verifier: IProviderVerifier, testInstance: Any)
}
/**
* Test target for HTTP tests. This is the default target.
*
* @property host Host to bind to. Defaults to localhost.
* @property port Port that the provider is running on. Defaults to 8080.
* @property path The path that the provider is mounted on. Defaults to the root path.
*/
open class HttpTestTarget @JvmOverloads constructor (
val host: String = "localhost",
val port: Int = 8080,
val path: String = "/"
) : TestTarget {
override fun isHttpTarget() = true
override fun getProviderInfo(serviceName: String, pactSource: PactSource?): ProviderInfo {
val providerInfo = ProviderInfo(serviceName)
providerInfo.port = port
providerInfo.host = host
providerInfo.protocol = "http"
providerInfo.path = path
return providerInfo
}
override fun prepareRequest(interaction: Interaction, context: Map): Pair? {
val providerClient = ProviderClient(getProviderInfo("provider"), HttpClientFactory())
if (interaction is RequestResponseInteraction) {
return providerClient.prepareRequest(interaction.request.generatedRequest(context)) to providerClient
}
throw UnsupportedOperationException("Only request/response interactions can be used with an HTTP test target")
}
override fun prepareVerifier(verifier: IProviderVerifier, testInstance: Any) { }
override fun executeInteraction(client: Any?, request: Any?): Map {
val providerClient = client as ProviderClient
val httpRequest = request as HttpUriRequest
return providerClient.executeRequest(providerClient.getHttpClient(), httpRequest)
}
companion object {
/**
* Creates a HttpTestTarget from a URL. If the URL does not contain a port, 8080 will be used.
*/
@JvmStatic
fun fromUrl(url: URL) = HttpTestTarget(url.host,
if (url.port == -1) 8080 else url.port,
if (url.path == null) "/" else url.path)
}
}
/**
* Test target for providers using HTTPS.
*
* @property host Host to bind to. Defaults to localhost.
* @property port Port that the provider is running on. Defaults to 8080.
* @property path The path that the provider is mounted on. Defaults to the root path.
* @property insecure Supports using certs that will not be verified. You need this enabled if you are using self-signed
* or untrusted certificates. Defaults to false.
*/
open class HttpsTestTarget @JvmOverloads constructor (
host: String = "localhost",
port: Int = 8443,
path: String = "",
val insecure: Boolean = false
) : HttpTestTarget(host, port, path) {
override fun getProviderInfo(serviceName: String, pactSource: PactSource?): ProviderInfo {
val providerInfo = super.getProviderInfo(serviceName, pactSource)
providerInfo.protocol = "https"
providerInfo.insecure = insecure
return providerInfo
}
companion object {
/**
* Creates a HttpsTestTarget from a URL. If the URL does not contain a port, 443 will be used.
*
* @param insecure Supports using certs that will not be verified. You need this enabled if you are using self-signed
* or untrusted certificates. Defaults to false.
*/
@JvmStatic
@JvmOverloads
fun fromUrl(url: URL, insecure: Boolean = false) = HttpsTestTarget(url.host,
if (url.port == -1) 443 else url.port, if (url.path == null) "/" else url.path, insecure)
}
}
/**
* Test target for use with asynchronous providers (like with message queues).
*
* This target will look for methods with a @PactVerifyProvider annotation where the value is the description of the
* interaction.
*
* @property packagesToScan List of packages to scan for methods with @PactVerifyProvider annotations. Defaults to the
* full test classpath.
*/
open class AmpqTestTarget(val packagesToScan: List = emptyList()) : TestTarget {
override fun isHttpTarget() = false
override fun getProviderInfo(serviceName: String, pactSource: PactSource?): ProviderInfo {
val providerInfo = ProviderInfo(serviceName)
providerInfo.verificationType = PactVerification.ANNOTATED_METHOD
providerInfo.packagesToScan = packagesToScan
if (pactSource is PactBrokerSource<*>) {
val (_, _, _, pacts) = pactSource
providerInfo.consumers = pacts.entries.flatMap { e -> e.value.map { p -> ConsumerInfo(e.key.name, p) } }
.toMutableList()
} else if (pactSource is DirectorySource<*>) {
val (_, pacts) = pactSource
providerInfo.consumers = pacts.entries.map { e -> ConsumerInfo(e.value.consumer.name, e.value) }
.toMutableList()
}
return providerInfo
}
override fun prepareRequest(interaction: Interaction, context: Map): Pair? {
if (interaction is Message) {
return null
}
throw UnsupportedOperationException("Only message interactions can be used with an AMPQ test target")
}
override fun prepareVerifier(verifier: IProviderVerifier, testInstance: Any) {
verifier.projectClasspath = Supplier {
when (val classLoader = testInstance.javaClass.classLoader) {
is URLClassLoader -> classLoader.urLs.toList()
else -> emptyList()
}
}
val defaultProviderMethodInstance = verifier.providerMethodInstance
verifier.providerMethodInstance = Function { m ->
if (m.declaringClass == testInstance.javaClass) {
testInstance
} else {
defaultProviderMethodInstance.apply(m)
}
}
}
override fun executeInteraction(client: Any?, request: Any?): Map {
return emptyMap()
}
}