Skip to content

Commit

Permalink
Merge branch 'async-message-test-template' of github.com:timvahlbrock…
Browse files Browse the repository at this point in the history
…/pact-jvm into timvahlbrock-async-message-test-template
  • Loading branch information
rholshausen committed Jul 31, 2024
2 parents bf66235 + a7251bd commit ff652bf
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package au.com.dius.pact.consumer.junit5

import au.com.dius.pact.core.model.V4Interaction
import org.junit.jupiter.api.extension.TestTemplateInvocationContext

class AsynchronousMessageContext(
val message: V4Interaction.AsynchronousMessage
): TestTemplateInvocationContext {
override fun getDisplayName(invocationIndex: Int): String {
return message.description
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,34 +24,29 @@ import au.com.dius.pact.core.model.annotations.PactFolder
import au.com.dius.pact.core.model.messaging.MessagePact
import au.com.dius.pact.core.support.Annotations
import au.com.dius.pact.core.support.BuiltToolConfig
import au.com.dius.pact.core.support.Json
import au.com.dius.pact.core.support.MetricEvent
import au.com.dius.pact.core.support.Metrics
import au.com.dius.pact.core.support.expressions.DataType
import au.com.dius.pact.core.support.expressions.ExpressionParser
import au.com.dius.pact.core.support.isNotEmpty
import io.github.oshai.kotlinlogging.KLogging
import org.apache.hc.core5.util.ReflectionUtils
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback
import org.junit.jupiter.api.extension.Extension
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.ParameterContext
import org.junit.jupiter.api.extension.ParameterResolver
import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.api.extension.*
import org.junit.platform.commons.support.AnnotationSupport
import org.junit.platform.commons.support.HierarchyTraversalMode
import org.junit.platform.commons.support.ReflectionSupport
import org.junit.platform.commons.util.AnnotationUtils.isAnnotated
import java.lang.reflect.Method
import java.util.Optional
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Stream
import kotlin.reflect.full.findAnnotation

class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCallback, ParameterResolver,
AfterTestExecutionCallback, AfterAllCallback {
AfterTestExecutionCallback, AfterAllCallback, TestTemplateInvocationContextProvider {

private val ep: ExpressionParser = ExpressionParser()

Expand Down Expand Up @@ -103,6 +98,21 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
return false
}

override fun supportsTestTemplate(extensionContext: ExtensionContext): Boolean {
val testTemplate = extensionContext
.testClass.get()
.methods
.find { AnnotationSupport.isAnnotated(it, TestTemplate::class.java) }

return testTemplate != null && testTemplate.parameters[0].type == AsynchronousMessageContext::class.java
}

override fun provideTestTemplateInvocationContexts(extensionContext: ExtensionContext): Stream<TestTemplateInvocationContext> {
val providerInfo = this.lookupProviderInfo(extensionContext)
val pact = setupPactForTest(providerInfo[0].first, providerInfo[0].second, extensionContext)
return pact.asV4Pact().unwrap().interactions.map { AsynchronousMessageContext(it.asAsynchronousMessage()!!) }.stream() as Stream<TestTemplateInvocationContext>
}

override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
val type = parameterContext.parameter.type
val providers = lookupProviderInfo(extensionContext)
Expand Down Expand Up @@ -259,36 +269,38 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
): BasePact {
val store = context.getStore(NAMESPACE)
val key = "pact:${providerInfo.providerName}"
var methods = pactMethods
if (methods.isEmpty()) {
methods = AnnotationSupport.findAnnotatedMethods(context.requiredTestClass, Pact::class.java, HierarchyTraversalMode.TOP_DOWN)
.map { m -> m.name}
}

return when {
store[key] != null -> store[key] as BasePact
else -> {
val pact = if (pactMethods.isEmpty()) {
lookupPact(providerInfo, "", context)
} else {
val head = pactMethods.first()
val tail = pactMethods.drop(1)
val initial = lookupPact(providerInfo, head, context)
tail.fold(initial) { acc, method ->
val pact = lookupPact(providerInfo, method, context)

if (pact.provider != acc.provider) {
// Should not really get here, as the Pacts should have been sorted by provider
throw IllegalArgumentException("You are using different Pacts with different providers for the same test" +
" ('${acc.provider}') and '${pact.provider}'). A separate test (and ideally a separate test class)" +
" should be used for each provider.")
}
val head = methods.first()
val tail = methods.drop(1)
val initial = lookupPact(providerInfo, head, context)
val pact = tail.fold(initial) { acc, method ->
val pact = lookupPact(providerInfo, method, context)

if (pact.provider != acc.provider) {
// Should not really get here, as the Pacts should have been sorted by provider
throw IllegalArgumentException("You are using different Pacts with different providers for the same test" +
" ('${acc.provider}') and '${pact.provider}'). A separate test (and ideally a separate test class)" +
" should be used for each provider.")
}

if (pact.consumer != acc.consumer) {
logger.warn {
"WARNING: You are using different Pacts with different consumers for the same test " +
"('${acc.consumer}') and '${pact.consumer}'). The second consumer will be ignored and dropped from " +
"the Pact and the interactions merged. If this is not your intention, you need to create a " +
"separate test for each consumer."
}
if (pact.consumer != acc.consumer) {
logger.warn {
"WARNING: You are using different Pacts with different consumers for the same test " +
"('${acc.consumer}') and '${pact.consumer}'). The second consumer will be ignored and dropped from " +
"the Pact and the interactions merged. If this is not your intention, you need to create a " +
"separate test for each consumer."
}

acc.mergeInteractions(pact.interactions) as BasePact
}

acc.mergeInteractions(pact.interactions) as BasePact
}
store.put(key, pact)
pact
Expand Down Expand Up @@ -541,7 +553,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
ProviderType.ASYNCH -> {
if (method.parameterTypes[0].isAssignableFrom(Class.forName("au.com.dius.pact.consumer.MessagePactBuilder"))) {
ReflectionSupport.invokeMethod(
method, context.requiredTestInstance,
method, context.testInstance,
MessagePactBuilder(providerInfo.pactVersion ?: PactSpecVersion.V3)
.consumer(pactConsumer).hasPactWith(providerNameToUse)
) as BasePact
Expand All @@ -550,7 +562,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
if (providerInfo.pactVersion != null) {
pactBuilder.pactSpecVersion(providerInfo.pactVersion)
}
ReflectionSupport.invokeMethod(method, context.requiredTestInstance, pactBuilder) as BasePact
ReflectionSupport.invokeMethod(method, context.testInstance, pactBuilder) as BasePact
}
}
ProviderType.SYNCH_MESSAGE -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package au.com.dius.pact.consumer.junit5

import au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody
import au.com.dius.pact.consumer.dsl.PactBuilder
import au.com.dius.pact.core.model.PactSpecVersion
import au.com.dius.pact.core.model.V4Interaction
import au.com.dius.pact.core.model.V4Pact
import au.com.dius.pact.core.model.annotations.Pact
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.notNullValue
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.api.extension.ExtendWith
import java.lang.IllegalStateException

@ExtendWith(value = [PactConsumerTestExt::class])
@PactTestFor(providerName = "checkout-service", providerType = ProviderType.ASYNCH, pactVersion = PactSpecVersion.V4)
class TestTemplateTest {
companion object {
private var reservationRan = false
private var cancellationRan = false

private val reservationBody = newJsonBody { o ->
o.stringType("purchaseId", "111")
o.stringType("name", "PURCHASE_STARTED")
o.eachLike("products", 1) { items ->
items.stringType("productID", "1")
items.stringType("productType", "FLIGHT")
items.stringType("availabilityId", "28e80c5987c6a242516ccdc004235b5e")
}
}.build()

private val cancelationBody = newJsonBody { o ->
o.stringType("purchaseId", "111")
o.stringType("reason", "user canceled")
}.build()

@JvmStatic
@Pact(consumer = "reservation-service", provider = "checkout-service")
fun pactForReservationBooking(builder: PactBuilder): V4Pact {
return builder
.usingLegacyMessageDsl()
.hasPactWith("checkout-service")
.expectsToReceive("a purchase started message to book a reservation")
.withContent(reservationBody)
.toPact()
}

@JvmStatic
@Pact(consumer = "reservation-service", provider = "checkout-service")
fun pactForCancellationBooking(builder: PactBuilder): V4Pact {
return builder
.usingLegacyMessageDsl()
.hasPactWith("checkout-service")
.expectsToReceive("a cancellation message to cancel a reservation")
.withContent(cancelationBody)
.toPact()
}

@JvmStatic
@AfterAll
fun makeSureAllRan() {
if(!reservationRan || !cancellationRan) {
throw IllegalStateException("Not all messages were tested.\nReservation: $reservationRan\nCancellation: $cancellationRan")
}
}
}

@TestTemplate
fun testPactForReservationBooking(context: AsynchronousMessageContext) {
assertThat(context.message, `is`(notNullValue()))
when (context.message.description) {
"a purchase started message to book a reservation" -> {
reservationRan = true
}
"a cancellation message to cancel a reservation" -> {
cancellationRan = true
}
else -> {
throw IllegalArgumentException("Unknown message description")
}
}
}
}

0 comments on commit ff652bf

Please sign in to comment.