com.trendyol.stove.testing.e2e.kafka.KafkaSystem.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of stove-spring-testing-e2e-kafka Show documentation
Show all versions of stove-spring-testing-e2e-kafka Show documentation
The easiest way of e2e testing in Kotlin
package com.trendyol.stove.testing.e2e.kafka
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.getOrElse
import com.trendyol.stove.functional.Try
import com.trendyol.stove.functional.recover
import com.trendyol.stove.testing.e2e.system.TestSystem
import com.trendyol.stove.testing.e2e.system.abstractions.ExposesConfiguration
import com.trendyol.stove.testing.e2e.system.abstractions.PluggedSystem
import com.trendyol.stove.testing.e2e.system.abstractions.RunnableSystemWithContext
import com.trendyol.stove.testing.e2e.system.abstractions.StateOfSystem
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import org.apache.kafka.clients.producer.ProducerRecord
import org.apache.kafka.common.header.internals.RecordHeader
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.getBean
import org.springframework.context.ApplicationContext
import org.springframework.kafka.core.KafkaTemplate
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
class KafkaSystem(
override val testSystem: TestSystem,
private val context: KafkaContext
) : PluggedSystem, RunnableSystemWithContext, ExposesConfiguration {
private val logger: Logger = LoggerFactory.getLogger(javaClass)
private lateinit var applicationContext: ApplicationContext
private lateinit var kafkaTemplate: KafkaTemplate
private lateinit var exposedConfiguration: KafkaExposedConfiguration
val getInterceptor = { applicationContext.getBean(TestSystemKafkaInterceptor::class.java) }
private val state: StateOfSystem =
StateOfSystem(testSystem.options, javaClass.kotlin, KafkaExposedConfiguration::class)
override suspend fun beforeRun() {}
override suspend fun run() {
exposedConfiguration = state.capture {
context.container.start()
KafkaExposedConfiguration(context.container.bootstrapServers)
}
}
override suspend fun afterRun(context: ApplicationContext) {
applicationContext = context
kafkaTemplate = context.getBean()
kafkaTemplate.setProducerListener(getInterceptor())
}
override fun configuration(): List = context.configureExposedConfiguration(exposedConfiguration) + listOf(
"kafka.bootstrapServers=${exposedConfiguration.bootstrapServers}",
"kafka.isSecure=false"
)
override suspend fun stop(): Unit = context.container.stop()
override fun close(): Unit = runBlocking {
Try {
kafkaTemplate.destroy()
executeWithReuseCheck { stop() }
}.recover {
logger.warn("got an error while closing KafkaSystem", it)
}
}
suspend fun publish(
topic: String,
message: Any,
key: Option = None,
headers: Map = mapOf(),
testCase: Option = None
): KafkaSystem {
val record = ProducerRecord(
topic,
0,
key.getOrElse { "" },
context.objectMapper.writeValueAsString(message),
headers.toMutableMap().addTestCase(testCase).map { RecordHeader(it.key, it.value.toByteArray()) }
)
return kafkaTemplate.usingCompletableFuture().send(record).await().let { this }
}
suspend fun shouldBeConsumed(
atLeastIn: Duration = 5.seconds,
message: Any
): KafkaSystem = coroutineScope {
shouldBeConsumedInternal(message::class, atLeastIn) { incomingMessage -> incomingMessage == Some(message) }
}.let { this }
suspend fun shouldBeFailed(
atLeastIn: Duration = 5.seconds,
message: Any,
exception: Throwable
): KafkaSystem = coroutineScope {
shouldBeFailedInternal(message::class, atLeastIn) { option, throwable -> option == Some(message) && throwable == exception }
}.let { this }
@PublishedApi
internal suspend fun shouldBeFailedOnCondition(
atLeastIn: Duration = 5.seconds,
condition: (T, Throwable) -> Boolean,
clazz: KClass
): KafkaSystem = coroutineScope {
shouldBeFailedInternal(clazz, atLeastIn) { message, throwable -> message.isSome { m -> condition(m, throwable) } }
}.let { this }
private suspend fun shouldBeFailedInternal(
clazz: KClass,
atLeastIn: Duration,
condition: (Option, Throwable) -> Boolean
): Unit = coroutineScope { getInterceptor().waitUntilFailed(atLeastIn, clazz, condition) }
@PublishedApi
internal suspend fun shouldBeConsumedOnCondition(
atLeastIn: Duration = 5.seconds,
condition: (T) -> Boolean,
clazz: KClass
): KafkaSystem = coroutineScope {
shouldBeConsumedInternal(clazz, atLeastIn) { incomingMessage -> incomingMessage.isSome { o -> condition(o) } }
}.let { this }
private suspend fun shouldBeConsumedInternal(
clazz: KClass,
atLeastIn: Duration,
condition: (Option) -> Boolean
): Unit = coroutineScope { getInterceptor().waitUntilConsumed(atLeastIn, clazz, condition) }
suspend fun shouldBePublished(
atLeastIn: Duration = 5.seconds,
message: Any
): KafkaSystem = coroutineScope {
shouldBePublishedInternal(message::class, atLeastIn) { incomingMessage -> incomingMessage == Some(message) }
}.let { this }
@PublishedApi
internal suspend fun shouldBePublishedOnCondition(
atLeastIn: Duration = 5.seconds,
condition: (T) -> Boolean,
clazz: KClass
): KafkaSystem = coroutineScope {
shouldBePublishedInternal(clazz, atLeastIn) { incomingMessage -> incomingMessage.isSome { o -> condition(o) } }
}.let { this }
private suspend fun shouldBePublishedInternal(
clazz: KClass,
atLeastIn: Duration,
condition: (Option) -> Boolean
): Unit = coroutineScope { getInterceptor().waitUntilPublished(atLeastIn, clazz, condition) }
companion object {
/**
* Extension for [KafkaSystem.shouldBeConsumedOnCondition] to enable generic invocation as method<[T]>(...)
*/
suspend inline fun KafkaSystem.shouldBeConsumedOnCondition(
atLeastIn: Duration = 5.seconds,
noinline condition: (T) -> Boolean
): KafkaSystem = this.shouldBeConsumedOnCondition(atLeastIn, condition, T::class)
/**
* Extension for [KafkaSystem.shouldBeFailedOnCondition] to enable generic invocation as method<[T]>(...)
*/
suspend inline fun KafkaSystem.shouldBeFailedOnCondition(
atLeastIn: Duration = 5.seconds,
noinline condition: (T, Throwable) -> Boolean
): KafkaSystem = this.shouldBeFailedOnCondition(atLeastIn, condition, T::class)
/**
* Extension for [KafkaSystem.shouldBePublishedOnCondition] to enable generic invocation as method<[T]>(...)
*/
suspend inline fun KafkaSystem.shouldBePublishedOnCondition(
atLeastIn: Duration = 5.seconds,
noinline condition: (T) -> Boolean
): KafkaSystem = this.shouldBePublishedOnCondition(atLeastIn, condition, T::class)
}
}
private fun (MutableMap).addTestCase(testCase: Option): MutableMap =
if (this.containsKey("testCase")) {
this
} else {
testCase.map { this["testCase"] = it }.let { this }
}