Polish "Add consumer test utility" (#592)
- update javadocs a bit - reorder some constructors based on specificity - add null check to "withSchema" - rename the test to end w/ "Tests" (plural) for consistency - simplified doc examples a bit w/ String message type - drop the exact match API - rename the conditions factory methods
This commit is contained in:
@@ -1,42 +1,46 @@
|
||||
[[testing-applications]]
|
||||
= Testing Applications
|
||||
|
||||
include::../attributes/attributes.adoc[]
|
||||
|
||||
The `spring-pulsar-test` dependency includes some useful utilities when testing your applications.
|
||||
|
||||
== PulsarConsumerTestUtil
|
||||
|
||||
`org.springframework.pulsar.test.support.PulsarConsumerTestUtil` provides a type-safe fluent API for consuming messages from a Pulsar topic within a test.
|
||||
The `org.springframework.pulsar.test.support.PulsarConsumerTestUtil` provides a type-safe fluent API for consuming messages from a Pulsar topic within a test.
|
||||
|
||||
[source,java]
|
||||
The following example shows how to consume messages from a topic for 5 seconds:
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
List<Message<MyMessage>> messages = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
List<Message<String>> messages = PulsarConsumerTestUtil.consumeMessages(consumerFactory)
|
||||
.fromTopic("my-topic")
|
||||
.withSchema(Schema.JSON(MyMessage.class))
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5))
|
||||
.get();
|
||||
----
|
||||
|
||||
A `until` method is also available to allow you to specify a condition that must be met before the messages are returned.
|
||||
An `until` method is also available to allow you to specify a condition that must be met before the messages are returned.
|
||||
The following example uses a condition to consume 5 messages from a topic.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
List<Message<MyMessage>> messages = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
List<Message<String>> messages = PulsarConsumerTestUtil.consumeMessages(consumerFactory)
|
||||
.fromTopic("my-topic")
|
||||
.withSchema(Schema.JSON(MyMessage.class))
|
||||
.until(messages -> messages.size() == 5)
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5))
|
||||
.until(messages -> messages.size() == 5)
|
||||
.get();
|
||||
----
|
||||
|
||||
A set of commonly used conditions are available in `org.springframework.pulsar.test.support.ConsumedMessagesConditions`.
|
||||
The following example uses the factory-provided `atLeastOneMessageMatches` condition to return the consumed messages once one of them has a value of `"boom"`.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
List<Message<MyMessage>> messages = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
List<Message<String>> messages = PulsarConsumerTestUtil.consumeMessages(consumerFactory)
|
||||
.fromTopic("my-topic")
|
||||
.withSchema(Schema.JSON(MyMessage.class))
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5))
|
||||
.until(containsExactlyExpectedValues(new MyMessage("foo"), new MyMessage("bar")))
|
||||
.until(ConsumedMessagesConditions.atLeastOneMessageMatches("boom"))
|
||||
.get();
|
||||
----
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
package org.springframework.pulsar.test.support;
|
||||
|
||||
/**
|
||||
* Exception thrown when a test times out.
|
||||
* Exception thrown when a condition was not fulfilled within the specified timeout.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
public class ConditionTimeoutException extends PulsarTestException {
|
||||
|
||||
public ConditionTimeoutException(String message, Throwable exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
|
||||
public ConditionTimeoutException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public ConditionTimeoutException(String message, Throwable exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ import java.util.List;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
|
||||
import org.springframework.pulsar.PulsarException;
|
||||
|
||||
/**
|
||||
* Assertions related step in the fluent API for building a Pulsar test consumer.
|
||||
* Conditions related step in the fluent API for building a Pulsar test consumer.
|
||||
*
|
||||
* @param <T> the type of the message payload
|
||||
* @author Jonas Geiregat
|
||||
@@ -32,27 +30,24 @@ import org.springframework.pulsar.PulsarException;
|
||||
public interface ConditionsSpec<T> {
|
||||
|
||||
/**
|
||||
* The maximum timeout duration to wait for the desired number of messages to be
|
||||
* reached.
|
||||
* @param timeout the maximum timeout duration to wait
|
||||
* The maximum amount of time to consume messages and wait for the condition to be
|
||||
* satisfied.
|
||||
* @param timeout the maximum amount of time for the condition to be met
|
||||
* @return the next step in the fluent API
|
||||
*/
|
||||
ConditionsSpec<T> awaitAtMost(Duration timeout);
|
||||
|
||||
/**
|
||||
* Start consuming until the given condition is met.
|
||||
* @param consumedMessagesCondition the condition to be met
|
||||
* Consume messages until the condition is satisfied.
|
||||
* @param condition the condition to be met
|
||||
* @return the next step in the fluent API
|
||||
*/
|
||||
ConditionsSpec<T> until(ConsumedMessagesCondition<T> consumedMessagesCondition);
|
||||
ConditionsSpec<T> until(ConsumedMessagesCondition<T> condition);
|
||||
|
||||
/**
|
||||
*
|
||||
* Terminal operation that will get the consumed messages within the timeout verifying
|
||||
* the given condition if any.
|
||||
* Terminal operation that begins the message consumption using the configured specs.
|
||||
* @return the consumed messages
|
||||
* @throws ConditionTimeoutException if the condition is not met within the timeout
|
||||
* @throws PulsarException if the condition is not met within the timeout
|
||||
*/
|
||||
List<Message<T>> get();
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import java.util.List;
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
|
||||
/**
|
||||
* A condition to be used in {@link PulsarConsumerTestUtil} to verify if it meets the
|
||||
* consumed messages.
|
||||
* A condition to be used in {@link PulsarConsumerTestUtil} to verify if the consumed
|
||||
* messages satisfy the given criteria.
|
||||
*
|
||||
* @param <T> the type of the message
|
||||
* @author Jonas Geiregat
|
||||
@@ -31,9 +31,9 @@ import org.apache.pulsar.client.api.Message;
|
||||
public interface ConsumedMessagesCondition<T> {
|
||||
|
||||
/**
|
||||
* Verifies that the consumed messages meet the condition.
|
||||
* Determines if the consumed messages meets the condition.
|
||||
* @param messages the consumed messages
|
||||
* @return {@code true} if the condition is met
|
||||
* @return whether the consumed messages meet the condition
|
||||
*/
|
||||
boolean meets(List<Message<T>> messages);
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ import org.apache.pulsar.client.api.Message;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Exposes a set of commonly used conditions to be used in {@link PulsarConsumerTestUtil}.
|
||||
* A factory for creating commonly used {@link ConsumedMessagesCondition conditions} that
|
||||
* can be used with {@link PulsarConsumerTestUtil}.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
@@ -41,42 +42,30 @@ public interface ConsumedMessagesConditions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that any of the consumed messages has a payload that equals the specified
|
||||
* value.
|
||||
* Verifies that the expected value equals the message payload value of at least one
|
||||
* consumed message.
|
||||
* @param expectation the expected value
|
||||
* @param <T> the type of the message
|
||||
* @return the condition
|
||||
*/
|
||||
static <T> ConsumedMessagesCondition<T> anyMessageMatchesExpected(T expectation) {
|
||||
return messages -> messages.stream().anyMatch(message -> message.getValue().equals(expectation));
|
||||
static <T> ConsumedMessagesCondition<T> atLeastOneMessageMatches(T expectation) {
|
||||
return messages -> messages.stream().map(Message::getValue).anyMatch(expectation::equals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the consumed messages value contains at all expected values.
|
||||
* Verifies that each expected value equals the message payload value of at least one
|
||||
* consumed message.
|
||||
* @param expectation the expected values
|
||||
* @param <T> the type of the message
|
||||
* @return the condition
|
||||
*/
|
||||
@SafeVarargs
|
||||
@SuppressWarnings("varargs")
|
||||
static <T> ConsumedMessagesCondition<T> containsAllExpectedValues(T... expectation) {
|
||||
static <T> ConsumedMessagesCondition<T> atLeastOneMessageMatchesEachOf(T... expectation) {
|
||||
return messages -> {
|
||||
var values = messages.stream().map(Message::getValue).toList();
|
||||
return Stream.of(expectation).allMatch(values::contains);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the consumed messages value contains exactly the expected values.
|
||||
* @param expectation the expected values
|
||||
* @param <T> the type of the message
|
||||
* @return the condition
|
||||
*/
|
||||
@SafeVarargs
|
||||
@SuppressWarnings("varargs")
|
||||
static <T> ConsumedMessagesCondition<T> containsExactlyExpectedValues(T... expectation) {
|
||||
return ConsumedMessagesConditions.<T>desiredMessageCount(expectation.length)
|
||||
.and(containsAllExpectedValues(expectation));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,8 +33,10 @@ import org.springframework.pulsar.core.PulsarConsumerFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Fluent API, to be used in tests, for consuming messages from Pulsar topics until a
|
||||
* certain {@code Condition} has been met.
|
||||
* Utility for consuming messages from Pulsar topics.
|
||||
* <p>
|
||||
* Exposes a Fluent builder-style API to construct the specifications for the message
|
||||
* consumption.
|
||||
*
|
||||
* @param <T> the type of the message payload
|
||||
* @author Jonas Geiregat
|
||||
@@ -67,6 +69,13 @@ public class PulsarConsumerTestUtil<T> implements TopicSpec<T>, SchemaSpec<T>, C
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionsSpec<T> withSchema(Schema<T> schema) {
|
||||
Assert.notNull(schema, "Schema must not be null");
|
||||
this.schema = schema;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionsSpec<T> awaitAtMost(Duration timeout) {
|
||||
Assert.notNull(timeout, "Timeout must not be null");
|
||||
@@ -80,17 +89,11 @@ public class PulsarConsumerTestUtil<T> implements TopicSpec<T>, SchemaSpec<T>, C
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionsSpec<T> withSchema(Schema<T> schema) {
|
||||
this.schema = schema;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message<T>> get() {
|
||||
var messages = new ArrayList<Message<T>>();
|
||||
try {
|
||||
String subscriptionName = UUID.randomUUID() + "-test-consumer";
|
||||
var subscriptionName = "test-consumer-%s".formatted(UUID.randomUUID());
|
||||
try (Consumer<T> consumer = consumerFactory.createConsumer(this.schema, this.topics, subscriptionName,
|
||||
c -> c.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest))) {
|
||||
long remainingMillis = timeout.toMillis();
|
||||
@@ -101,10 +104,8 @@ public class PulsarConsumerTestUtil<T> implements TopicSpec<T>, SchemaSpec<T>, C
|
||||
messages.add(message);
|
||||
consumer.acknowledge(message);
|
||||
}
|
||||
if (this.condition != null) {
|
||||
if (this.condition.meets(messages)) {
|
||||
return messages;
|
||||
}
|
||||
if (this.condition != null && this.condition.meets(messages)) {
|
||||
return messages;
|
||||
}
|
||||
remainingMillis -= System.currentTimeMillis() - loopStartTime;
|
||||
}
|
||||
@@ -115,7 +116,8 @@ public class PulsarConsumerTestUtil<T> implements TopicSpec<T>, SchemaSpec<T>, C
|
||||
throw new PulsarException(ex);
|
||||
}
|
||||
if (this.condition != null && !this.condition.meets(messages)) {
|
||||
throw new ConditionTimeoutException("Condition was not met within " + timeout.toSeconds() + " seconds");
|
||||
throw new ConditionTimeoutException(
|
||||
"Condition was not met within %d seconds".formatted(timeout.toSeconds()));
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -16,20 +16,22 @@
|
||||
|
||||
package org.springframework.pulsar.test.support;
|
||||
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a test fails.
|
||||
* Generic exception thrown when something related to testing fails.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
public class PulsarTestException extends RuntimeException {
|
||||
|
||||
public PulsarTestException(String message, Throwable exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
public class PulsarTestException extends NestedRuntimeException {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public PulsarTestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PulsarTestException(String message, Throwable exception) {
|
||||
super(message, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.junit.jupiter.api.Test;
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
class ConsumedMessagesConditionTest {
|
||||
class ConsumedMessagesConditionTests {
|
||||
|
||||
@Test
|
||||
void bothConditionShouldBeMetInOrderForAChainedAndConditionToBeMet() {
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.test.support;
|
||||
|
||||
import static org.springframework.pulsar.test.support.ConsumedMessagesConditions.desiredMessageCount;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.client.impl.MessageImpl;
|
||||
import org.apache.pulsar.common.api.proto.MessageMetadata;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConsumedMessagesConditions}.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
class ConsumedMessagesConditionsTest {
|
||||
|
||||
private List<Message<String>> createStringMessages(int count) {
|
||||
return IntStream.range(0, count)
|
||||
.<Message<String>>mapToObj(i -> MessageImpl.create(new MessageMetadata(),
|
||||
ByteBuffer.wrap(("message-" + i).getBytes()), Schema.STRING, "topic"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DesiredMessageCountTests {
|
||||
|
||||
@Test
|
||||
void receivedMessageCountMeetCondition() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = desiredMessageCount(2);
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(2));
|
||||
|
||||
Assertions.assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivedMessageCountDoesNotMeetCondition() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = desiredMessageCount(3);
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(2));
|
||||
|
||||
Assertions.assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = { 0, -1 })
|
||||
void throwExceptionWhenDesiredMessageCountEqualOrLessThanZero(int messageCount) {
|
||||
Assertions.assertThatThrownBy(() -> desiredMessageCount(messageCount))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("Desired message count must be greater than 0");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AnyMessageMatchesExpectedConditionTests {
|
||||
|
||||
@Test
|
||||
void messageValuesContainsExpectation() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = ConsumedMessagesConditions
|
||||
.anyMessageMatchesExpected("message-1");
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(3));
|
||||
|
||||
Assertions.assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageValuesDoesNotContainExpectation() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = ConsumedMessagesConditions
|
||||
.anyMessageMatchesExpected("message-3");
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(3));
|
||||
|
||||
Assertions.assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ContainsAllExpectedValuesConditionTests {
|
||||
|
||||
@Test
|
||||
void messageValuesContainsExpectation() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = ConsumedMessagesConditions
|
||||
.containsAllExpectedValues("message-1", "message-2");
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(3));
|
||||
|
||||
Assertions.assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageValuesDoesNotContainExpectation() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = ConsumedMessagesConditions
|
||||
.containsAllExpectedValues("message-3", "message-4");
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(3));
|
||||
|
||||
Assertions.assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ContainsExactlyExpectedValuesConditionTests {
|
||||
|
||||
@Test
|
||||
void messageValuesContainsExpectations() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = ConsumedMessagesConditions
|
||||
.containsExactlyExpectedValues("message-0", "message-1");
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(2));
|
||||
|
||||
Assertions.assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageValuesDoesNotContainExpectation() {
|
||||
ConsumedMessagesCondition<String> consumedMessagesCondition = ConsumedMessagesConditions
|
||||
.containsExactlyExpectedValues("message-0", "message-1");
|
||||
|
||||
boolean result = consumedMessagesCondition.meets(createStringMessages(3));
|
||||
|
||||
Assertions.assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.test.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.springframework.pulsar.test.support.ConsumedMessagesConditions.desiredMessageCount;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.apache.pulsar.client.impl.MessageImpl;
|
||||
import org.apache.pulsar.common.api.proto.MessageMetadata;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConsumedMessagesConditions}.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
class ConsumedMessagesConditionsTests {
|
||||
|
||||
private List<Message<String>> createStringMessages(int count) {
|
||||
return IntStream.range(0, count)
|
||||
.<Message<String>>mapToObj(i -> MessageImpl.create(new MessageMetadata(),
|
||||
ByteBuffer.wrap(("message-" + i).getBytes()), Schema.STRING, "topic"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class DesiredMessageCount {
|
||||
|
||||
@Test
|
||||
void receivedMessageCountMeetCondition() {
|
||||
ConsumedMessagesCondition<String> condition = desiredMessageCount(2);
|
||||
var messages = createStringMessages(2);
|
||||
assertThat(condition.meets(messages)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivedMessageCountDoesNotMeetCondition() {
|
||||
ConsumedMessagesCondition<String> condition = desiredMessageCount(3);
|
||||
var messages = createStringMessages(2);
|
||||
assertThat(condition.meets(messages)).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = { 0, -1 })
|
||||
void throwExceptionWhenDesiredMessageCountEqualOrLessThanZero(int messageCount) {
|
||||
assertThatIllegalStateException().isThrownBy(() -> desiredMessageCount(messageCount))
|
||||
.withMessage("Desired message count must be greater than 0");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AtLeaseOneMessageMatches {
|
||||
|
||||
@Test
|
||||
void messageValuesContainsExpectation() {
|
||||
ConsumedMessagesCondition<String> condition = ConsumedMessagesConditions
|
||||
.atLeastOneMessageMatches("message-1");
|
||||
var messages = createStringMessages(3);
|
||||
assertThat(condition.meets(messages)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageValuesDoesNotContainExpectation() {
|
||||
ConsumedMessagesCondition<String> condition = ConsumedMessagesConditions
|
||||
.atLeastOneMessageMatches("message-5");
|
||||
var messages = createStringMessages(3);
|
||||
assertThat(condition.meets(messages)).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class AtLeaseOneMessageMatchesEachOf {
|
||||
|
||||
@Test
|
||||
void messageValuesContainsExpectation() {
|
||||
ConsumedMessagesCondition<String> condition = ConsumedMessagesConditions
|
||||
.atLeastOneMessageMatchesEachOf("message-1", "message-2");
|
||||
var messages = createStringMessages(3);
|
||||
assertThat(condition.meets(messages)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageValuesDoesNotContainExpectation() {
|
||||
ConsumedMessagesCondition<String> condition = ConsumedMessagesConditions
|
||||
.atLeastOneMessageMatchesEachOf("message-3", "message-4");
|
||||
var messages = createStringMessages(3);
|
||||
assertThat(condition.meets(messages)).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.test.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.springframework.pulsar.test.support.ConsumedMessagesConditions.desiredMessageCount;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.PulsarClientException;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarConsumerTestUtil}.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
class PulsarConsumerTestUtilTests implements PulsarTestContainerSupport {
|
||||
|
||||
private PulsarTemplate<Object> pulsarTemplate;
|
||||
|
||||
private DefaultPulsarConsumerFactory<String> pulsarConsumerFactory;
|
||||
|
||||
private static String testTopic(String suffix) {
|
||||
return "ptctut-topic-" + suffix;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void prepareForTest() throws PulsarClientException {
|
||||
var pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl()).build();
|
||||
this.pulsarConsumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, List.of());
|
||||
this.pulsarTemplate = new PulsarTemplate<>(new DefaultPulsarProducerFactory<>(pulsarClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenConditionIsSpecifiedMessagesAreConsumedUntilConditionIsMet() {
|
||||
var topic = testTopic("a");
|
||||
IntStream.range(0, 5).forEach(i -> pulsarTemplate.send(topic, "message-" + i));
|
||||
var msgs = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic(topic)
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5))
|
||||
.until(desiredMessageCount(3))
|
||||
.get();
|
||||
assertThat(msgs).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenConditionIsNotSpecifiedMessagesAreConsumedUntilAwaitDuration() {
|
||||
var topic = testTopic("b");
|
||||
IntStream.range(0, 5).forEach(i -> pulsarTemplate.send(topic, "message-" + i));
|
||||
var msgs = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic(topic)
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5))
|
||||
.until(null)
|
||||
.get();
|
||||
assertThat(msgs).extracting(Message::getValue)
|
||||
.containsExactlyInAnyOrderElementsOf(IntStream.range(0, 5).mapToObj(i -> "message-" + i).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionIsThrownWhenConditionNotMetWithinAwaitDuration() {
|
||||
assertThatExceptionOfType(ConditionTimeoutException.class)
|
||||
.isThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic(testTopic("c"))
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5))
|
||||
.until(ConsumedMessagesConditions.desiredMessageCount(3))
|
||||
.get())
|
||||
.withMessage("Condition was not met within 5 seconds");
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumerFactoryCannotBeNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(null))
|
||||
.withMessage("PulsarConsumerFactory must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void topicCannotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory).fromTopic(null))
|
||||
.withMessage("Topic must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaCannotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-foo")
|
||||
.withSchema(null))
|
||||
.withMessage("Schema must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void awaitAtMostCannotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-foo")
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(null))
|
||||
.withMessage("Timeout must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.pulsar.test.support;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.pulsar.test.support.ConsumedMessagesConditions.desiredMessageCount;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.pulsar.client.api.Message;
|
||||
import org.apache.pulsar.client.api.PulsarClient;
|
||||
import org.apache.pulsar.client.api.PulsarClientException;
|
||||
import org.apache.pulsar.client.api.Schema;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.pulsar.core.DefaultPulsarConsumerFactory;
|
||||
import org.springframework.pulsar.core.DefaultPulsarProducerFactory;
|
||||
import org.springframework.pulsar.core.PulsarTemplate;
|
||||
|
||||
/**
|
||||
* Tests for {@link PulsarConsumerTestUtil}.
|
||||
*
|
||||
* @author Jonas Geiregat
|
||||
*/
|
||||
class PulsarTestConsumerTestUtilTest implements PulsarTestContainerSupport {
|
||||
|
||||
private PulsarTemplate<Object> pulsarTemplate;
|
||||
|
||||
private DefaultPulsarConsumerFactory<String> pulsarConsumerFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws PulsarClientException {
|
||||
var pulsarClient = PulsarClient.builder().serviceUrl(PulsarTestContainerSupport.getPulsarBrokerUrl()).build();
|
||||
this.pulsarConsumerFactory = new DefaultPulsarConsumerFactory<>(pulsarClient, List.of());
|
||||
this.pulsarTemplate = new PulsarTemplate<>(new DefaultPulsarProducerFactory<>(pulsarClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumerFactoryCannotBeNull() {
|
||||
assertThatThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("PulsarConsumerFactory must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void topicCannotBeNull() {
|
||||
assertThatThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory).fromTopic(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Topic must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void awaitAtMostTimeoutCannotBeNull() {
|
||||
assertThatThrownBy(() -> PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-a")
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(null)).isInstanceOf(IllegalArgumentException.class).hasMessage("Timeout must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void untilConditionCanBeNull() {
|
||||
var testConsumer = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-b")
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(2))
|
||||
.until(null);
|
||||
|
||||
pulsarTemplate.send("topic-b", "message");
|
||||
|
||||
assertThat(testConsumer.get()).hasSize(1).map(Message::getValue).containsExactly("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumerReturnsWhenConditionIsMet() {
|
||||
var testConsumer = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-c")
|
||||
.withSchema(Schema.STRING);
|
||||
|
||||
IntStream.range(0, 10).forEach(i -> pulsarTemplate.send("topic-c", "message-" + i));
|
||||
|
||||
List<Message<String>> messages = testConsumer.until(desiredMessageCount(2)).get();
|
||||
|
||||
assertThat(messages).hasSize(2).map(Message::getValue).containsExactly("message-0", "message-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumerReturnsAllMessagesWhenNoConditionIsPresent() {
|
||||
var testConsumer = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-d")
|
||||
.withSchema(Schema.STRING);
|
||||
|
||||
IntStream.range(0, 10).forEach(i -> pulsarTemplate.send("topic-d", "message-" + i));
|
||||
|
||||
List<Message<String>> messages = testConsumer.get();
|
||||
|
||||
assertThat(messages).hasSize(10)
|
||||
.map(Message::getValue)
|
||||
.containsExactlyElementsOf(IntStream.range(0, 10).mapToObj(i -> "message-" + i).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwExceptionWhenConditionIsNotMet() {
|
||||
var testConsumer = PulsarConsumerTestUtil.consumeMessages(pulsarConsumerFactory)
|
||||
.fromTopic("topic-e")
|
||||
.withSchema(Schema.STRING)
|
||||
.awaitAtMost(Duration.ofSeconds(5));
|
||||
|
||||
ThrowingCallable consume = () -> testConsumer.until(desiredMessageCount(20)).get();
|
||||
|
||||
assertThatThrownBy(consume).isInstanceOf(ConditionTimeoutException.class)
|
||||
.hasMessage("Condition was not met within 5 seconds");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user