Add IT for SpEL topics on @PulsarMessage

This commit adds a lightweight integration test nested in the
DefaultTopicResolverTests which uses a small Spring context to verify
actual expression evaluation.

See #568
This commit is contained in:
Chris Bono
2024-02-14 00:02:57 -06:00
parent 18378a8927
commit ca44f0bab3
3 changed files with 78 additions and 30 deletions

View File

@@ -25,6 +25,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
* {@link ConfigurableBeanFactory} capabilities to resolve expressions.
*
* @author Jonas Geiregat
* @since 1.1.0
*/
public class DefaultExpressionResolver implements ExpressionResolver {
@@ -40,6 +41,13 @@ public class DefaultExpressionResolver implements ExpressionResolver {
this.configurableBeanFactory = configurableBeanFactory;
}
/**
* {@inheritDoc}
* @param expression the expression to resolve (can include property placeholders and
* SpEL)
* @return a {@code Resolved} instance containing the resolved string value (can be
* null) or an exception if the resolution failed.
*/
@Override
public Resolved<String> resolveToString(String expression) {
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(expression);
@@ -51,7 +59,7 @@ public class DefaultExpressionResolver implements ExpressionResolver {
return Resolved.of(null);
}
return Resolved
.failed("The expression '%s' must resolve to a string but was: %s".formatted(expression, resolvedObj));
.failed("The expression '%s' must resolve to a string but was: %s".formatted(expression, resolvedObj));
}
}

View File

@@ -17,16 +17,18 @@
package org.springframework.pulsar.core;
/**
* Resolves expressions.
* Strategy interface for resolving a value from a string expression.
*
* @author Jonas Geiregat
* @since 1.1.0
*/
public interface ExpressionResolver {
/**
* Resolve the given expression.
* Resolve the given expression as a string value.
* @param expression the expression to resolve
* @return the resolved value as a {@code Resolved} of {@link String}
* @return a {@code Resolved} instance containing the resolved string value (can be
* null) or an exception if the resolution failed.
*/
Resolved<String> resolveToString(String expression);

View File

@@ -18,11 +18,9 @@ package org.springframework.pulsar.core;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.stream.Stream;
@@ -35,9 +33,17 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import org.springframework.pulsar.annotation.PulsarMessage;
import org.springframework.pulsar.core.DefaultTopicResolverTests.TopicByAnnotatedMessageType.WithTopicExpression.WithTopicExpressionConfig;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Unit tests for {@link DefaultTopicResolver}.
@@ -138,8 +144,6 @@ class DefaultTopicResolverTests {
@Nested
class TopicByAnnotatedMessageType {
private static final String bazTopicExpression = "#{someExpression}";
@Test
void customMappingTakesPrecedenceOverAnnotationMapping() {
assertThat(resolver.resolveTopic(null, Baz.class, () -> defaultTopic).value().orElse(null))
@@ -169,29 +173,63 @@ class DefaultTopicResolverTests {
verify(resolver, times(1)).getAnnotatedTopicInfo(Baz.class);
}
@Test
void annotatedMessageTypeWithTopicExpressionIsResolved() {
var mockExpressionResolver = mock(ExpressionResolver.class);
when(mockExpressionResolver.resolveToString(bazTopicExpression)).thenReturn(Resolved.of(bazTopic));
var expressionTopicResolver = new DefaultTopicResolver(mockExpressionResolver);
assertThat(expressionTopicResolver.resolveTopic(null, BazWithTopicExpression.class,
() -> defaultTopic).value().orElse(null))
.isEqualTo(bazTopic);
verify(mockExpressionResolver, times(1)).resolveToString(bazTopicExpression);
}
/**
* Lightweight integration tests for the expression resolver functionality in
* {@link DefaultTopicResolver}.
* <p>
* Starts up a small Spring context which in turns provides the bean factory and
* expression resolver to the topic resolver.
*/
@Nested
@SpringJUnitConfig
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
@ContextConfiguration(classes = WithTopicExpressionConfig.class)
@TestPropertySource(properties = { "app.customPropertyTopic = my-custom-property-topic" })
class WithTopicExpression {
@Test
void deriveExpressionResolverFromBeanFactory() {
var mockBeanFactory = mock(ConfigurableBeanFactory.class);
var expressionTopicResolver = new DefaultTopicResolver();
expressionTopicResolver.setBeanFactory(mockBeanFactory);
assertThat(expressionTopicResolver)
.extracting("expressionResolver")
.isNotNull();
}
// @formatter:off
@Test
void propertyPlaceholderExpressionIsResolved(@Autowired DefaultTopicResolver topicResolver) {
assertThat(topicResolver.resolveTopic(null, MsgTypeWithTopicPropertyExpression.class, () -> defaultTopic)
.value().orElse(null)).isEqualTo("my-custom-property-topic");
}
@Test
void spelExpressionIsResolved(@Autowired DefaultTopicResolver topicResolver) {
assertThat(topicResolver.resolveTopic(null, MsgTypeWithTopicSpELExpression.class, () -> defaultTopic)
.value().orElse(null)).isEqualTo("my-custom-spel-topic");
}
@Test
void embeddedExpressionIsResolved(@Autowired DefaultTopicResolver topicResolver) {
assertThat(topicResolver.resolveTopic(null, MsgTypeWithTopicEmbeddedExpression.class, () -> defaultTopic)
.value().orElse(null)).isEqualTo("my-custom-property-topic".toUpperCase());
}
// @formatter:on
@Configuration(proxyBeanMethods = false)
static class WithTopicExpressionConfig {
@Bean
DefaultTopicResolver defaultTopicResolver() {
return new DefaultTopicResolver();
}
}
@PulsarMessage(topic = "${app.customPropertyTopic}")
record MsgTypeWithTopicPropertyExpression(String value) {
}
@PulsarMessage(topic = "#{T(java.lang.String).valueOf('my-custom-spel-topic')}")
record MsgTypeWithTopicSpELExpression(String value) {
}
@PulsarMessage(topic = "#{T(java.lang.String).valueOf('${app.customPropertyTopic}').toUpperCase()}")
record MsgTypeWithTopicEmbeddedExpression(String value) {
}
@PulsarMessage(topic = bazTopicExpression)
record BazWithTopicExpression(String value) {
}
}