From ec34aee4de20c63679fbb34f3b4aafd6bd405943 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sun, 14 Jul 2024 14:58:45 +0200 Subject: [PATCH 1/5] PoC --- .../junit5/AsynchronousMessageContext.kt | 9 +++++ .../consumer/junit5/PactConsumerTestExt.kt | 33 ++++++++++++------- 2 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt diff --git a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt new file mode 100644 index 000000000..92c913bff --- /dev/null +++ b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt @@ -0,0 +1,9 @@ +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 { +} \ No newline at end of file diff --git a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt index 719b2676f..4d92ccd31 100644 --- a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt +++ b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt @@ -24,7 +24,6 @@ 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 @@ -33,14 +32,8 @@ import au.com.dius.pact.core.support.isNotEmpty import io.github.oshai.kotlinlogging.KLogging 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 @@ -48,10 +41,11 @@ 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() @@ -103,6 +97,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 == V4Interaction.AsynchronousMessage::class.java + } + + override fun provideTestTemplateInvocationContexts(extensionContext: ExtensionContext): Stream { + 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 + } + override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any { val type = parameterContext.parameter.type val providers = lookupProviderInfo(extensionContext) @@ -541,7 +550,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 @@ -550,7 +559,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 -> { From 59c874079ed080170d8fa31262d48cd488c0c03d Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sun, 14 Jul 2024 15:59:57 +0200 Subject: [PATCH 2/5] Have multiple test cases be run by template --- .../junit5/AsynchronousMessageContext.kt | 3 + .../consumer/junit5/PactConsumerTestExt.kt | 51 ++++++++------- .../pact/consumer/junit5/TestTemplateTest.kt | 62 +++++++++++++++++++ 3 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt diff --git a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt index 92c913bff..762ef73ed 100644 --- a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt +++ b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/AsynchronousMessageContext.kt @@ -6,4 +6,7 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContext class AsynchronousMessageContext( val message: V4Interaction.AsynchronousMessage ): TestTemplateInvocationContext { + override fun getDisplayName(invocationIndex: Int): String { + return message.description + } } \ No newline at end of file diff --git a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt index 4d92ccd31..77c84662e 100644 --- a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt +++ b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt @@ -30,6 +30,7 @@ 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.TestTemplate @@ -268,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 diff --git a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt new file mode 100644 index 000000000..50755ec8f --- /dev/null +++ b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt @@ -0,0 +1,62 @@ +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.equalTo +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.notNullValue +import org.junit.jupiter.api.TestTemplate +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(value = [PactConsumerTestExt::class]) +@PactTestFor(providerName = "checkout-service", providerType = ProviderType.ASYNCH, pactVersion = PactSpecVersion.V4) +class TestTemplateTest { + companion object { + 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() + } + } + + @TestTemplate + fun testPactForReservationBooking(message: V4Interaction.AsynchronousMessage) { + assertThat(message, `is`(notNullValue())) + } +} From 82711345397708b47d95ade93cd851784670df27 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sun, 14 Jul 2024 16:05:10 +0200 Subject: [PATCH 3/5] throw error if not all message types were tested --- .../pact/consumer/junit5/TestTemplateTest.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt index 50755ec8f..cb13f658c 100644 --- a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt +++ b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt @@ -7,16 +7,20 @@ 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.equalTo 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") @@ -53,10 +57,25 @@ class TestTemplateTest { .withContent(cancelationBody) .toPact() } + + @JvmStatic + @AfterAll + fun makeSureAllRan() { + if(!reservationRan || !cancellationRan) { + throw IllegalStateException("Not all messages were tested.\nReservation: $reservationRan\nCancellation: $cancellationRan") + } + } } @TestTemplate fun testPactForReservationBooking(message: V4Interaction.AsynchronousMessage) { assertThat(message, `is`(notNullValue())) + if (message.description == "a purchase started message to book a reservation") { + reservationRan = true + } else if(message.description == "a cancellation message to cancel a reservation") { + cancellationRan = true + } else { + throw IllegalArgumentException("Unknown message description") + } } } From de83916dcd372b88fb15177cc8d27011a9ebe026 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sun, 14 Jul 2024 16:05:43 +0200 Subject: [PATCH 4/5] minor refactoring --- .../pact/consumer/junit5/TestTemplateTest.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt index cb13f658c..1662653a5 100644 --- a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt +++ b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt @@ -70,12 +70,16 @@ class TestTemplateTest { @TestTemplate fun testPactForReservationBooking(message: V4Interaction.AsynchronousMessage) { assertThat(message, `is`(notNullValue())) - if (message.description == "a purchase started message to book a reservation") { - reservationRan = true - } else if(message.description == "a cancellation message to cancel a reservation") { - cancellationRan = true - } else { - throw IllegalArgumentException("Unknown message description") + when (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") + } } } } From a7251bda067735f305abdff1ece9a47606ddb3d1 Mon Sep 17 00:00:00 2001 From: Tim Vahlbrock Date: Sun, 14 Jul 2024 16:14:10 +0200 Subject: [PATCH 5/5] inject message context (fails) --- .../au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt | 2 +- .../au/com/dius/pact/consumer/junit5/TestTemplateTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt index 77c84662e..938980509 100644 --- a/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt +++ b/consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt @@ -104,7 +104,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal .methods .find { AnnotationSupport.isAnnotated(it, TestTemplate::class.java) } - return testTemplate != null && testTemplate.parameters[0].type == V4Interaction.AsynchronousMessage::class.java + return testTemplate != null && testTemplate.parameters[0].type == AsynchronousMessageContext::class.java } override fun provideTestTemplateInvocationContexts(extensionContext: ExtensionContext): Stream { diff --git a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt index 1662653a5..e25a048c8 100644 --- a/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt +++ b/consumer/junit5/src/test/kotlin/au/com/dius/pact/consumer/junit5/TestTemplateTest.kt @@ -68,9 +68,9 @@ class TestTemplateTest { } @TestTemplate - fun testPactForReservationBooking(message: V4Interaction.AsynchronousMessage) { - assertThat(message, `is`(notNullValue())) - when (message.description) { + fun testPactForReservationBooking(context: AsynchronousMessageContext) { + assertThat(context.message, `is`(notNullValue())) + when (context.message.description) { "a purchase started message to book a reservation" -> { reservationRan = true }