Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Test Templates for Async Message Tests #1815

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}
}
}
}
Loading