diff --git a/.gitignore b/.gitignore index c2065bc..a8c1bde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ HELP.md +.kotlin .gradle build/ !gradle/wrapper/gradle-wrapper.jar diff --git a/README.md b/README.md index 2e4a9e2..6a0e57c 100644 --- a/README.md +++ b/README.md @@ -220,20 +220,20 @@ class MyApplicationIntegrationTest { @MockBean // We mock MyEventConsumer lateinit var eventConsumer: MyEventConsumer - @Test - fun `consume event`() { + @Test + fun `should consume event`() { + val eventCaptor = argumentCaptor() + doNothing().`when`(eventConsumer).consume(eventCaptor.capture()) + // We send a Kafka message using a helper val text = "hello ${UUID.randomUUID()}" kafkaProducerHelper.send(TOPIC, "{\"number\":${text.length},\"string\":\"$text\"}") // We wait at most 5 seconds to receive the expected MyEvent in MyEventConsumer mock - val eventCaptor = argumentCaptor() - verify(eventConsumer, timeout(FIVE_SECONDS.toMillis())).consume(eventCaptor.capture()) - - assertThat(eventCaptor.firstValue).satisfies { event -> - assertThat(event.text).isEqualTo(text) - } - } + await().atMost(TEN_SECONDS).untilAsserted { + assertThat(eventCaptor.allValues.filter { it.text == text }).isEqualTo(ONE) + } + } } ``` * Check the complete test in [MyApplicationIntegrationTest.kt](src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt). @@ -278,7 +278,7 @@ spring: And we can test it like this: ```kotlin @Test -fun `produce event`() { +fun `should produce event`() { val text = "hello ${UUID.randomUUID()}" eventProducer.produce(MyEvent(text)) @@ -326,19 +326,18 @@ spring: And we can test it like this: ```kotlin @Test -fun `retry consume event 5 times`() { +fun `should retry consume event 5 times`() { // we throw a MyRetryableException every time we receive a message - doThrow(MyRetryableException("retry later!")).`when`(eventConsumer).consume(any()) + val eventCaptor = argumentCaptor() + doThrow(MyRetryableException("retry later!")).`when`(eventConsumer).consume(eventCaptor.capture()) // we send a Kafka message using a helper val text = "hello ${UUID.randomUUID()}" kafkaProducerHelper.send(TOPIC, "{\"number\":${text.length},\"string\":\"$text\"}") // consumer has been called five times with the same message - val eventCaptor = argumentCaptor() - verify(eventConsumer, timeout(TEN_SECONDS.toMillis()).times(FIVE)).consume(eventCaptor.capture()) - assertThat(eventCaptor.allValues).allSatisfy { event -> - assertThat(event.text).isEqualTo(text) + await().atMost(TEN_SECONDS).untilAsserted { + assertThat(eventCaptor.allValues.filter { it.text == text }).isEqualTo(FIVE) } } ``` @@ -384,7 +383,7 @@ And we can test it like this: #### Application errors: ```kotlin @Test -fun `send to DLQ rejected messages`() { +fun `should send to DLQ rejected messages`() { // we throw a MyRetryableException every time we receive a message doThrow(MyRetryableException("retry later!")).`when`(eventConsumer).consume(any()) diff --git a/build.gradle.kts b/build.gradle.kts index e8548ae..87671a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,8 +11,6 @@ plugins { group = "com.rogervinas" version = "0.0.1-SNAPSHOT" -java.sourceCompatibility = JavaVersion.VERSION_21 -java.targetCompatibility = JavaVersion.VERSION_21 repositories { mavenCentral() @@ -21,6 +19,12 @@ repositories { val springCloudVersion = "2023.0.2" val testContainersVersion = "1.19.8" +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.cloud:spring-cloud-starter-stream-kafka") @@ -35,6 +39,7 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("com.nhaarman:mockito-kotlin:1.6.0") + testImplementation("org.awaitility:awaitility:4.2.1") } dependencyManagement { @@ -44,9 +49,8 @@ dependencyManagement { } tasks.withType { - kotlinOptions { + compilerOptions { freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = "21" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e09..6f7a6eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85..7101f8e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt b/src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt index d16cefc..29ebc8a 100644 --- a/src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt +++ b/src/test/kotlin/com/rogervinas/stream/MyApplicationIntegrationTest.kt @@ -1,23 +1,20 @@ package com.rogervinas.stream -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.argumentCaptor -import com.nhaarman.mockito_kotlin.doThrow -import com.nhaarman.mockito_kotlin.timeout -import com.nhaarman.mockito_kotlin.verify -import com.rogervinas.stream.helper.DockerComposeContainerHelper -import com.rogervinas.stream.helper.KafkaConsumerHelper -import com.rogervinas.stream.helper.KafkaProducerHelper +import com.nhaarman.mockito_kotlin.* import com.rogervinas.stream.domain.MyEvent import com.rogervinas.stream.domain.MyEventConsumer import com.rogervinas.stream.domain.MyEventProducer import com.rogervinas.stream.domain.MyRetryableException +import com.rogervinas.stream.helper.DockerComposeContainerHelper +import com.rogervinas.stream.helper.KafkaConsumerHelper +import com.rogervinas.stream.helper.KafkaProducerHelper import org.assertj.core.api.Assertions.assertThat +import org.awaitility.Awaitility.await +import org.awaitility.Durations.TEN_SECONDS import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import org.mockito.Mockito.reset import org.skyscreamer.jsonassert.JSONAssert import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier @@ -25,11 +22,11 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.boot.test.mock.mockito.MockReset import org.springframework.test.context.ActiveProfiles import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers -import java.time.Duration -import java.util.UUID +import java.util.* import java.util.function.Consumer @SpringBootTest(webEnvironment = NONE) @@ -41,7 +38,7 @@ class MyApplicationIntegrationTest { private const val TOPIC = "my.topic" private const val TOPIC_DLQ = "my.topic.errors" - private val TEN_SECONDS = Duration.ofSeconds(10) + private const val ONE = 1 private const val FIVE = 5 @Container @@ -52,7 +49,7 @@ class MyApplicationIntegrationTest { @Qualifier("myStreamEventProducer") // Avoid SpringBootTest issue: expected single matching bean but found 2 lateinit var eventProducer: MyEventProducer - @MockBean + @MockBean(reset = MockReset.BEFORE) lateinit var eventConsumer: MyEventConsumer @Value("\${spring.cloud.stream.kafka.binder.brokers}") @@ -63,7 +60,6 @@ class MyApplicationIntegrationTest { @BeforeEach fun setUp() { - reset(eventConsumer) kafkaConsumerHelper = KafkaConsumerHelper(kafkaBroker, TOPIC) kafkaConsumerHelper.consumeAll() kafkaDLQConsumerHelper = KafkaConsumerHelper(kafkaBroker, TOPIC_DLQ) @@ -86,26 +82,30 @@ class MyApplicationIntegrationTest { @Test fun `should consume event`() { + val eventCaptor = argumentCaptor() + doNothing().`when`(eventConsumer).consume(eventCaptor.capture()) + val text = "hello ${UUID.randomUUID()}" kafkaProducerHelper.send(TOPIC, "{\"number\":${text.length},\"string\":\"$text\"}") - val eventCaptor = argumentCaptor() - verify(eventConsumer, timeout(TEN_SECONDS.toMillis())).consume(eventCaptor.capture()) + verify(eventConsumer, timeout(TEN_SECONDS.toMillis())).consume(any()) - assertThat(eventCaptor.firstValue).satisfies(Consumer { event -> assertThat(event.text).isEqualTo(text) }) + await().atMost(TEN_SECONDS).untilAsserted { + assertThat(eventCaptor.allValues.filter { it.text == text }).hasSize(ONE) + } } @Test fun `should retry consume event 5 times`() { - doThrow(MyRetryableException("retry later!")).`when`(eventConsumer).consume(any()) + val eventCaptor = argumentCaptor() + doThrow(MyRetryableException("retry later!")).`when`(eventConsumer).consume(eventCaptor.capture()) val text = "hello ${UUID.randomUUID()}" kafkaProducerHelper.send(TOPIC, "{\"number\":${text.length},\"string\":\"$text\"}") - val eventCaptor = argumentCaptor() - verify(eventConsumer, timeout(TEN_SECONDS.toMillis()).times(FIVE)).consume(eventCaptor.capture()) - - assertThat(eventCaptor.allValues).allSatisfy(Consumer { event -> assertThat(event.text).isEqualTo(text) }) + await().atMost(TEN_SECONDS).untilAsserted { + assertThat(eventCaptor.allValues.filter { it.text == text }).hasSize(FIVE) + } } @Test