Skip to content

Commit

Permalink
feat: implemented pending pact support with JUnit 4 #1033
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Holshausen committed May 18, 2020
1 parent dbd8109 commit 6aacf98
Show file tree
Hide file tree
Showing 17 changed files with 208 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package au.com.dius.pact.provider.junit

import au.com.dius.pact.core.model.BrokerUrlSource
import au.com.dius.pact.core.model.FilteredPact
import au.com.dius.pact.core.model.Interaction
import au.com.dius.pact.core.model.Pact
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.core.model.ProviderState
import au.com.dius.pact.core.pactbroker.TestResult
import au.com.dius.pact.provider.DefaultTestResultAccumulator
import au.com.dius.pact.provider.IProviderVerifier
import au.com.dius.pact.provider.ProviderUtils
import au.com.dius.pact.provider.TestResultAccumulator
import au.com.dius.pact.provider.VerificationFailureType
import au.com.dius.pact.provider.VerificationResult
import au.com.dius.pact.provider.junit.descriptions.DescriptionGenerator
import au.com.dius.pact.provider.junitsupport.target.Target
import au.com.dius.pact.provider.junit.target.TestClassAwareTarget
import au.com.dius.pact.provider.junitsupport.target.TestTarget
import au.com.dius.pact.provider.junitsupport.MissingStateChangeMethod
import au.com.dius.pact.provider.junitsupport.State
import au.com.dius.pact.provider.junitsupport.TargetRequestFilter
import au.com.dius.pact.provider.junitsupport.target.Target
import au.com.dius.pact.provider.junitsupport.target.TestTarget
import mu.KLogging
import org.junit.After
import org.junit.Before
Expand Down Expand Up @@ -54,7 +56,7 @@ open class InteractionRunner<I>(
private val pactSource: PactSource
) : Runner() where I : Interaction {

private val results = ConcurrentHashMap<String, Pair<Boolean, IProviderVerifier>>()
private val results = ConcurrentHashMap<String, Pair<VerificationResult, IProviderVerifier>>()
private val testContext = ConcurrentHashMap<String, Any>()
private val childDescriptions = ConcurrentHashMap<String, Description>()
private val descriptionGenerator = DescriptionGenerator(testClass, pact, pactSource)
Expand All @@ -73,15 +75,15 @@ open class InteractionRunner<I>(
return description
}

protected fun describeChild(interaction: Interaction): Description {
private fun describeChild(interaction: Interaction): Description {
if (!childDescriptions.containsKey(interaction.uniqueKey())) {
childDescriptions[interaction.uniqueKey()] = descriptionGenerator.generate(interaction)
}
return childDescriptions[interaction.uniqueKey()]!!
}

// Validation
protected fun validate() {
private fun validate() {
val errors = mutableListOf<Throwable>()

validatePublicVoidNoArgMethods(Before::class.java, false, errors)
Expand All @@ -103,15 +105,15 @@ open class InteractionRunner<I>(
}
}

protected fun validatePublicVoidNoArgMethods(
private fun validatePublicVoidNoArgMethods(
annotation: Class<out Annotation>,
isStatic: Boolean,
errors: MutableList<Throwable>
) {
testClass.getAnnotatedMethods(annotation).forEach { method -> method.validatePublicVoidNoArg(isStatic, errors) }
}

protected fun validateConstructor(errors: MutableList<Throwable>) {
private fun validateConstructor(errors: MutableList<Throwable>) {
if (!hasOneConstructor()) {
errors.add(Exception("Test class should have exactly one public constructor"))
}
Expand All @@ -121,9 +123,9 @@ open class InteractionRunner<I>(
}
}

protected fun hasOneConstructor() = testClass.javaClass.kotlin.constructors.size == 1
private fun hasOneConstructor() = testClass.javaClass.kotlin.constructors.size == 1

protected fun validateTestTarget(errors: MutableList<Throwable>) {
private fun validateTestTarget(errors: MutableList<Throwable>) {
val annotatedFields = testClass.getAnnotatedFields(TestTarget::class.java)
if (annotatedFields.size != 1) {
errors.add(Exception("Test class should have exactly one field annotated with ${TestTarget::class.java.name}"))
Expand All @@ -133,7 +135,7 @@ open class InteractionRunner<I>(
}
}

protected fun validateRules(errors: List<Throwable>) {
private fun validateRules(errors: List<Throwable>) {
RULE_VALIDATOR.validate(testClass, errors)
RULE_METHOD_VALIDATOR.validate(testClass, errors)
}
Expand All @@ -142,21 +144,29 @@ open class InteractionRunner<I>(
override fun run(notifier: RunNotifier) {
for (interaction in pact.interactions) {
val description = describeChild(interaction)
notifier.fireTestStarted(description)
var testResult: TestResult = TestResult.Ok
var testResult: VerificationResult = VerificationResult.Ok
val pending = pact.source is BrokerUrlSource && (pact.source as BrokerUrlSource).result?.pending == true
if (!pending) {
notifier.fireTestStarted(description)
} else {
notifier.fireTestIgnored(description)
}
try {
interactionBlock(interaction, pactSource, testContext).evaluate()
notifier.fireTestFinished(description)
} catch (e: Throwable) {
notifier.fireTestFailure(Failure(description, e))
testResult = TestResult.Failed(listOf(mapOf("message" to "Request to provider failed with an exception",
if (!pending) {
notifier.fireTestFailure(Failure(description, e))
}
testResult = VerificationResult.Failed(listOf(mapOf("message" to "Request to provider failed with an exception",
"exception" to e, "interactionId" to interaction.interactionId)),
"Request to provider failed with an exception")
"Request to provider failed with an exception", description.displayName,
listOf(VerificationFailureType.ExceptionFailure(e)), pending)
} finally {
notifier.fireTestFinished(description)
if (pact is FilteredPact) {
testResultAccumulator.updateTestResult(pact.pact, interaction, testResult, pactSource)
testResultAccumulator.updateTestResult(pact.pact, interaction, testResult.toTestResult(), pactSource)
} else {
testResultAccumulator.updateTestResult(pact, interaction, testResult, pactSource)
testResultAccumulator.updateTestResult(pact, interaction, testResult.toTestResult(), pactSource)
}
}
}
Expand Down Expand Up @@ -203,7 +213,6 @@ open class InteractionRunner<I>(
target.addResultCallback(BiConsumer { result, verifier ->
results[interaction.uniqueKey()] = Pair(result, verifier)
})
surrogateTestMethod()
target.testInteraction(pact.consumer.name, interaction, source, mapOf("providerState" to context))
}
}
Expand All @@ -214,8 +223,6 @@ open class InteractionRunner<I>(
return statement
}

fun surrogateTestMethod() { }

protected open fun setupTargetForInteraction(target: Target) { }

protected fun lookupTarget(testInstance: Any): Target {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ open class PactRunner<I>(private val clazz: Class<*>) : ParentRunner<Interaction
if (clazz.getAnnotation(Ignore::class.java) != null) {
logger.info("Ignore annotation detected, exiting")
} else {

val providerInfo = clazz.getAnnotation(Provider::class.java) ?: throw InitializationError(
"Provider name should be specified by using ${Provider::class.java.simpleName} annotation")
val serviceName = providerInfo.value
Expand Down Expand Up @@ -124,7 +123,7 @@ open class PactRunner<I>(private val clazz: Class<*>) : ParentRunner<Interaction

protected open fun setupInteractionRunners(testClass: TestClass, pacts: List<Pact<I>>, pactLoader: PactLoader) {
for (pact in pacts) {
this.children.add(newInteractionRunner(testClass, pact, pactLoader.pactSource))
this.children.add(newInteractionRunner(testClass, pact, pact.source))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,32 @@ class DescriptionGenerator<I : Interaction>(
*/
fun generate(interaction: Interaction): Description {
return Description.createTestDescription(testClass.javaClass,
"${pact.consumer.name} ${this.getTagDescription()}- Upon ${interaction.description}")
"${consumerName()} ${this.getTagDescription()}- Upon ${interaction.description}${this.pending()}")
}

private fun getTagDescription(): String {
private fun consumerName(): String {
return if (pact.source is BrokerUrlSource) {
val source = pact.source as BrokerUrlSource
source.result?.name ?: pact.consumer.name
} else {
pact.consumer.name
}
}

private fun pending(): String {
return if (pact.source is BrokerUrlSource) {
val source = pact.source as BrokerUrlSource
if (source.result != null && source.result!!.pending) {
" <PENDING>"
} else {
""
}
} else {
""
}
}

private fun getTagDescription(): String {
if (pactSource is BrokerUrlSource && pactSource.tag.isNotEmpty()) {
return "[tag:${pactSource.tag}] "
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ 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.provider.ConsumerInfo
import au.com.dius.pact.provider.IConsumerInfo
import au.com.dius.pact.provider.IProviderInfo
import au.com.dius.pact.provider.IProviderVerifier
import au.com.dius.pact.provider.PactVerification
import au.com.dius.pact.provider.ProviderInfo
import au.com.dius.pact.provider.ProviderVerifier
import au.com.dius.pact.provider.VerificationResult
import au.com.dius.pact.provider.junitsupport.Provider
import mu.KLogging
import java.net.URLClassLoader
Expand Down Expand Up @@ -36,18 +39,17 @@ open class AmqpTarget @JvmOverloads constructor(
context: Map<String, Any>
) {
val provider = getProviderInfo(source)
val consumer = ConsumerInfo(consumerName)
val verifier = setupVerifier(interaction, provider, consumer)
val consumer = consumerInfo(consumerName, source)
val verifier = setupVerifier(interaction, provider, consumer, source)

val failures = mutableMapOf<String, Any>()
verifier.verifyResponseByInvokingProviderMethods(provider, consumer, interaction, interaction.description,
failures)
reportTestResult(failures.isEmpty(), verifier)
val result = verifier.verifyResponseByInvokingProviderMethods(provider, consumer, interaction,
interaction.description, mutableMapOf())
reportTestResult(result, verifier)

try {
if (failures.isNotEmpty()) {
verifier.displayFailures(failures)
throw getAssertionError(failures)
if (result is VerificationResult.Failed) {
verifier.displayFailures(listOf(result))
throw AssertionError(verifier.generateErrorStringFromVerificationResult(listOf(result)))
}
} finally {
verifier.finaliseReports()
Expand All @@ -56,8 +58,9 @@ open class AmqpTarget @JvmOverloads constructor(

override fun setupVerifier(
interaction: Interaction,
provider: ProviderInfo,
consumer: ConsumerInfo
provider: IProviderInfo,
consumer: IConsumerInfo,
pactSource: PactSource?
): IProviderVerifier {
val verifier = ProviderVerifier()
verifier.projectClasspath = Supplier {
Expand All @@ -76,10 +79,10 @@ open class AmqpTarget @JvmOverloads constructor(
}
}

setupReporters(verifier, provider.name, interaction.description)
setupReporters(verifier)

verifier.initialiseReporters(provider)
verifier.reportVerificationForConsumer(consumer, provider, null)
verifier.reportVerificationForConsumer(consumer, provider, pactSource)

if (interaction.providerStates.isNotEmpty()) {
for ((name) in interaction.providerStates) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package au.com.dius.pact.provider.junit.target

import au.com.dius.pact.core.model.BrokerUrlSource
import au.com.dius.pact.core.model.Interaction
import au.com.dius.pact.core.model.PactSource
import au.com.dius.pact.core.support.expressions.SystemPropertyResolver
import au.com.dius.pact.core.support.expressions.ValueResolver
import au.com.dius.pact.provider.ConsumerInfo
import au.com.dius.pact.provider.IConsumerInfo
import au.com.dius.pact.provider.IProviderInfo
import au.com.dius.pact.provider.IProviderVerifier
import au.com.dius.pact.provider.ProviderInfo
import au.com.dius.pact.provider.junitsupport.JUnitProviderTestSupport
import au.com.dius.pact.provider.VerificationResult
import au.com.dius.pact.provider.junitsupport.VerificationReports
import au.com.dius.pact.provider.reporters.ReporterManager
import au.com.dius.pact.core.support.expressions.SystemPropertyResolver
import au.com.dius.pact.core.support.expressions.ValueResolver
import au.com.dius.pact.provider.junitsupport.target.Target
import au.com.dius.pact.provider.reporters.AnsiConsoleReporter
import au.com.dius.pact.provider.reporters.ReporterManager
import org.apache.commons.lang3.tuple.Pair
import org.junit.runners.model.FrameworkMethod
import org.junit.runners.model.TestClass
import java.io.File
import java.util.function.BiConsumer
import java.util.function.Supplier
import org.apache.commons.lang3.tuple.Pair
import org.junit.runners.model.FrameworkMethod

/**
* Out-of-the-box implementation of [Target],
Expand All @@ -28,18 +32,19 @@ abstract class BaseTarget : TestClassAwareTarget {
protected lateinit var testTarget: Any

var valueResolver: ValueResolver = SystemPropertyResolver()
private val callbacks = mutableListOf<BiConsumer<Boolean, IProviderVerifier>>()
private val callbacks = mutableListOf<BiConsumer<VerificationResult, IProviderVerifier>>()
private val stateHandlers = mutableListOf<Pair<Class<out Any>, Supplier<out Any>>>()

protected abstract fun getProviderInfo(source: PactSource): ProviderInfo

protected abstract fun setupVerifier(
interaction: Interaction,
provider: ProviderInfo,
consumer: ConsumerInfo
provider: IProviderInfo,
consumer: IConsumerInfo,
pactSource: PactSource?
): IProviderVerifier

protected fun setupReporters(verifier: IProviderVerifier, name: String, description: String) {
protected fun setupReporters(verifier: IProviderVerifier) {
var reportDirectory = "target/pact/reports"
var reportingEnabled = false

Expand All @@ -61,29 +66,32 @@ abstract class BaseTarget : TestClassAwareTarget {
if (reportingEnabled) {
val reportDir = File(reportDirectory)
reportDir.mkdirs()
verifier.reporters = reports
val reporters = reports
.filter { r -> r.isNotEmpty() }
.map { r ->
val reporter = ReporterManager.createReporter(r.trim(), reportDir, verifier)
reporter
}
if (reporters.none { it is AnsiConsoleReporter }) {
verifier.reporters = listOf(ReporterManager.createReporter("console", null, verifier)) + reporters
} else {
verifier.reporters = reporters
}
} else {
verifier.reporters = listOf(ReporterManager.createReporter("console", null, verifier))
}
}

protected fun getAssertionError(mismatches: Map<String, Any>): AssertionError {
return AssertionError(JUnitProviderTestSupport.generateErrorStringFromMismatches(mismatches))
}

override fun setTestClass(testClass: TestClass, testTarget: Any) {
this.testClass = testClass
this.testTarget = testTarget
}

override fun addResultCallback(callback: BiConsumer<Boolean, IProviderVerifier>) {
override fun addResultCallback(callback: BiConsumer<VerificationResult, IProviderVerifier>) {
this.callbacks.add(callback)
}

protected fun reportTestResult(result: Boolean, verifier: IProviderVerifier) {
protected fun reportTestResult(result: VerificationResult, verifier: IProviderVerifier) {
this.callbacks.forEach { callback -> callback.accept(result, verifier) }
}

Expand Down Expand Up @@ -113,4 +121,18 @@ abstract class BaseTarget : TestClassAwareTarget {
}
}
}

protected fun consumerInfo(consumerName: String, source: PactSource): IConsumerInfo {
return when (source) {
is BrokerUrlSource -> {
val brokerResult = source.result
if (brokerResult != null) {
ConsumerInfo(consumerName, pactSource = source, notices = brokerResult.notices, pending = brokerResult.pending)
} else {
ConsumerInfo(consumerName, pactSource = source)
}
}
else -> ConsumerInfo(consumerName, pactSource = source)
}
}
}
Loading

0 comments on commit 6aacf98

Please sign in to comment.