Skip to content

Commit ff652bf

Browse files
committed
Merge branch 'async-message-test-template' of github.com:timvahlbrock/pact-jvm into timvahlbrock-async-message-test-template
2 parents bf66235 + a7251bd commit ff652bf

File tree

3 files changed

+145
-36
lines changed

3 files changed

+145
-36
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package au.com.dius.pact.consumer.junit5
2+
3+
import au.com.dius.pact.core.model.V4Interaction
4+
import org.junit.jupiter.api.extension.TestTemplateInvocationContext
5+
6+
class AsynchronousMessageContext(
7+
val message: V4Interaction.AsynchronousMessage
8+
): TestTemplateInvocationContext {
9+
override fun getDisplayName(invocationIndex: Int): String {
10+
return message.description
11+
}
12+
}

consumer/junit5/src/main/kotlin/au/com/dius/pact/consumer/junit5/PactConsumerTestExt.kt

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,34 +24,29 @@ import au.com.dius.pact.core.model.annotations.PactFolder
2424
import au.com.dius.pact.core.model.messaging.MessagePact
2525
import au.com.dius.pact.core.support.Annotations
2626
import au.com.dius.pact.core.support.BuiltToolConfig
27-
import au.com.dius.pact.core.support.Json
2827
import au.com.dius.pact.core.support.MetricEvent
2928
import au.com.dius.pact.core.support.Metrics
3029
import au.com.dius.pact.core.support.expressions.DataType
3130
import au.com.dius.pact.core.support.expressions.ExpressionParser
3231
import au.com.dius.pact.core.support.isNotEmpty
3332
import io.github.oshai.kotlinlogging.KLogging
33+
import org.apache.hc.core5.util.ReflectionUtils
3434
import org.junit.jupiter.api.Disabled
3535
import org.junit.jupiter.api.Nested
36-
import org.junit.jupiter.api.extension.AfterAllCallback
37-
import org.junit.jupiter.api.extension.AfterTestExecutionCallback
38-
import org.junit.jupiter.api.extension.BeforeAllCallback
39-
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback
40-
import org.junit.jupiter.api.extension.Extension
41-
import org.junit.jupiter.api.extension.ExtensionContext
42-
import org.junit.jupiter.api.extension.ParameterContext
43-
import org.junit.jupiter.api.extension.ParameterResolver
36+
import org.junit.jupiter.api.TestTemplate
37+
import org.junit.jupiter.api.extension.*
4438
import org.junit.platform.commons.support.AnnotationSupport
4539
import org.junit.platform.commons.support.HierarchyTraversalMode
4640
import org.junit.platform.commons.support.ReflectionSupport
4741
import org.junit.platform.commons.util.AnnotationUtils.isAnnotated
4842
import java.lang.reflect.Method
4943
import java.util.Optional
5044
import java.util.concurrent.ConcurrentHashMap
45+
import java.util.stream.Stream
5146
import kotlin.reflect.full.findAnnotation
5247

5348
class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCallback, ParameterResolver,
54-
AfterTestExecutionCallback, AfterAllCallback {
49+
AfterTestExecutionCallback, AfterAllCallback, TestTemplateInvocationContextProvider {
5550

5651
private val ep: ExpressionParser = ExpressionParser()
5752

@@ -103,6 +98,21 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
10398
return false
10499
}
105100

101+
override fun supportsTestTemplate(extensionContext: ExtensionContext): Boolean {
102+
val testTemplate = extensionContext
103+
.testClass.get()
104+
.methods
105+
.find { AnnotationSupport.isAnnotated(it, TestTemplate::class.java) }
106+
107+
return testTemplate != null && testTemplate.parameters[0].type == AsynchronousMessageContext::class.java
108+
}
109+
110+
override fun provideTestTemplateInvocationContexts(extensionContext: ExtensionContext): Stream<TestTemplateInvocationContext> {
111+
val providerInfo = this.lookupProviderInfo(extensionContext)
112+
val pact = setupPactForTest(providerInfo[0].first, providerInfo[0].second, extensionContext)
113+
return pact.asV4Pact().unwrap().interactions.map { AsynchronousMessageContext(it.asAsynchronousMessage()!!) }.stream() as Stream<TestTemplateInvocationContext>
114+
}
115+
106116
override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any {
107117
val type = parameterContext.parameter.type
108118
val providers = lookupProviderInfo(extensionContext)
@@ -259,36 +269,38 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
259269
): BasePact {
260270
val store = context.getStore(NAMESPACE)
261271
val key = "pact:${providerInfo.providerName}"
272+
var methods = pactMethods
273+
if (methods.isEmpty()) {
274+
methods = AnnotationSupport.findAnnotatedMethods(context.requiredTestClass, Pact::class.java, HierarchyTraversalMode.TOP_DOWN)
275+
.map { m -> m.name}
276+
}
277+
262278
return when {
263279
store[key] != null -> store[key] as BasePact
264280
else -> {
265-
val pact = if (pactMethods.isEmpty()) {
266-
lookupPact(providerInfo, "", context)
267-
} else {
268-
val head = pactMethods.first()
269-
val tail = pactMethods.drop(1)
270-
val initial = lookupPact(providerInfo, head, context)
271-
tail.fold(initial) { acc, method ->
272-
val pact = lookupPact(providerInfo, method, context)
273-
274-
if (pact.provider != acc.provider) {
275-
// Should not really get here, as the Pacts should have been sorted by provider
276-
throw IllegalArgumentException("You are using different Pacts with different providers for the same test" +
277-
" ('${acc.provider}') and '${pact.provider}'). A separate test (and ideally a separate test class)" +
278-
" should be used for each provider.")
279-
}
281+
val head = methods.first()
282+
val tail = methods.drop(1)
283+
val initial = lookupPact(providerInfo, head, context)
284+
val pact = tail.fold(initial) { acc, method ->
285+
val pact = lookupPact(providerInfo, method, context)
286+
287+
if (pact.provider != acc.provider) {
288+
// Should not really get here, as the Pacts should have been sorted by provider
289+
throw IllegalArgumentException("You are using different Pacts with different providers for the same test" +
290+
" ('${acc.provider}') and '${pact.provider}'). A separate test (and ideally a separate test class)" +
291+
" should be used for each provider.")
292+
}
280293

281-
if (pact.consumer != acc.consumer) {
282-
logger.warn {
283-
"WARNING: You are using different Pacts with different consumers for the same test " +
284-
"('${acc.consumer}') and '${pact.consumer}'). The second consumer will be ignored and dropped from " +
285-
"the Pact and the interactions merged. If this is not your intention, you need to create a " +
286-
"separate test for each consumer."
287-
}
294+
if (pact.consumer != acc.consumer) {
295+
logger.warn {
296+
"WARNING: You are using different Pacts with different consumers for the same test " +
297+
"('${acc.consumer}') and '${pact.consumer}'). The second consumer will be ignored and dropped from " +
298+
"the Pact and the interactions merged. If this is not your intention, you need to create a " +
299+
"separate test for each consumer."
288300
}
289-
290-
acc.mergeInteractions(pact.interactions) as BasePact
291301
}
302+
303+
acc.mergeInteractions(pact.interactions) as BasePact
292304
}
293305
store.put(key, pact)
294306
pact
@@ -541,7 +553,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
541553
ProviderType.ASYNCH -> {
542554
if (method.parameterTypes[0].isAssignableFrom(Class.forName("au.com.dius.pact.consumer.MessagePactBuilder"))) {
543555
ReflectionSupport.invokeMethod(
544-
method, context.requiredTestInstance,
556+
method, context.testInstance,
545557
MessagePactBuilder(providerInfo.pactVersion ?: PactSpecVersion.V3)
546558
.consumer(pactConsumer).hasPactWith(providerNameToUse)
547559
) as BasePact
@@ -550,7 +562,7 @@ class PactConsumerTestExt : Extension, BeforeTestExecutionCallback, BeforeAllCal
550562
if (providerInfo.pactVersion != null) {
551563
pactBuilder.pactSpecVersion(providerInfo.pactVersion)
552564
}
553-
ReflectionSupport.invokeMethod(method, context.requiredTestInstance, pactBuilder) as BasePact
565+
ReflectionSupport.invokeMethod(method, context.testInstance, pactBuilder) as BasePact
554566
}
555567
}
556568
ProviderType.SYNCH_MESSAGE -> {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package au.com.dius.pact.consumer.junit5
2+
3+
import au.com.dius.pact.consumer.dsl.LambdaDsl.newJsonBody
4+
import au.com.dius.pact.consumer.dsl.PactBuilder
5+
import au.com.dius.pact.core.model.PactSpecVersion
6+
import au.com.dius.pact.core.model.V4Interaction
7+
import au.com.dius.pact.core.model.V4Pact
8+
import au.com.dius.pact.core.model.annotations.Pact
9+
import org.hamcrest.MatcherAssert.assertThat
10+
import org.hamcrest.Matchers.`is`
11+
import org.hamcrest.Matchers.notNullValue
12+
import org.junit.jupiter.api.AfterAll
13+
import org.junit.jupiter.api.TestTemplate
14+
import org.junit.jupiter.api.extension.ExtendWith
15+
import java.lang.IllegalStateException
16+
17+
@ExtendWith(value = [PactConsumerTestExt::class])
18+
@PactTestFor(providerName = "checkout-service", providerType = ProviderType.ASYNCH, pactVersion = PactSpecVersion.V4)
19+
class TestTemplateTest {
20+
companion object {
21+
private var reservationRan = false
22+
private var cancellationRan = false
23+
24+
private val reservationBody = newJsonBody { o ->
25+
o.stringType("purchaseId", "111")
26+
o.stringType("name", "PURCHASE_STARTED")
27+
o.eachLike("products", 1) { items ->
28+
items.stringType("productID", "1")
29+
items.stringType("productType", "FLIGHT")
30+
items.stringType("availabilityId", "28e80c5987c6a242516ccdc004235b5e")
31+
}
32+
}.build()
33+
34+
private val cancelationBody = newJsonBody { o ->
35+
o.stringType("purchaseId", "111")
36+
o.stringType("reason", "user canceled")
37+
}.build()
38+
39+
@JvmStatic
40+
@Pact(consumer = "reservation-service", provider = "checkout-service")
41+
fun pactForReservationBooking(builder: PactBuilder): V4Pact {
42+
return builder
43+
.usingLegacyMessageDsl()
44+
.hasPactWith("checkout-service")
45+
.expectsToReceive("a purchase started message to book a reservation")
46+
.withContent(reservationBody)
47+
.toPact()
48+
}
49+
50+
@JvmStatic
51+
@Pact(consumer = "reservation-service", provider = "checkout-service")
52+
fun pactForCancellationBooking(builder: PactBuilder): V4Pact {
53+
return builder
54+
.usingLegacyMessageDsl()
55+
.hasPactWith("checkout-service")
56+
.expectsToReceive("a cancellation message to cancel a reservation")
57+
.withContent(cancelationBody)
58+
.toPact()
59+
}
60+
61+
@JvmStatic
62+
@AfterAll
63+
fun makeSureAllRan() {
64+
if(!reservationRan || !cancellationRan) {
65+
throw IllegalStateException("Not all messages were tested.\nReservation: $reservationRan\nCancellation: $cancellationRan")
66+
}
67+
}
68+
}
69+
70+
@TestTemplate
71+
fun testPactForReservationBooking(context: AsynchronousMessageContext) {
72+
assertThat(context.message, `is`(notNullValue()))
73+
when (context.message.description) {
74+
"a purchase started message to book a reservation" -> {
75+
reservationRan = true
76+
}
77+
"a cancellation message to cancel a reservation" -> {
78+
cancellationRan = true
79+
}
80+
else -> {
81+
throw IllegalArgumentException("Unknown message description")
82+
}
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)