Add MockMessageHandler to the Testing Framework

Fix PayloadMatcherTests for generics

Address PR comments and other improvements

* Revert `rawtypes` mode for the `PayloadMatcher`
* Make `HeaderMatcher` as `rawtypes` as well
* Make `MockMessageHandler` expect `rawtypes` for `Matcher`s.
This way we can just support `Matcher`s like `notNullValue(Message.class)`
* Rename `expect()` to `assertNext()`
* Rename `andReply()` to `thenReply()`
* Track replies are supplied in the `MockMessageHandler`
* Distinguish simple `MH` from the `MP` types in the
`MockIntegrationContext#instead()` do not let to replace simple `MH`
with fully configured `MockMessageHandler` or any other `MP` implementation.
Fail replace if types mismatch; wrap `MockMessageHandler` to simple `MH`
if it doesn't have replies when we are going to replace simple `MH`
* Wrap `MockMessageHandler` to the `Mockito.spy()` in the
`MockIntegration#mockMessageHandler()` to allow to `verify()` interaction
in the test-case

Remove wrapping `MockMH` to raw `MH` when no reply supported.
If `MockMH` isn't supplied with replies ti's safe to use it as is - no harm to target endpoint
which supposed to be last one in the flow

Some polishing and JavaDocs

More JavaDocs

Add docs for the `MockMessageHandler` and fix some JavaDocs

Make the `MockMessageHandler` with an API like:
```
MockIntegration.mockMessageHandler()
             .handleNext(Consumer<Message<?>>)
             .handleNext(Consumer<Message<?>>)
             .handleNextAndReply(Function<Message<?>, Object>)
             .handleNext(Consumer<Message<?>>)
             .handleNextAndReply(Function<Message<?>, Object>)
             .handleNextAndReply(Function<Message<?>, Object>);
```

Doc Polishing
This commit is contained in:
Artem Bilan
2017-05-08 18:19:49 -04:00
committed by Gary Russell
parent ea6cf0f4ef
commit 079ccb84e2
13 changed files with 577 additions and 126 deletions

View File

@@ -27,8 +27,14 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.Lifecycle;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.endpoint.IntegrationConsumer;
import org.springframework.integration.endpoint.SourcePollingChannelAdapter;
import org.springframework.integration.test.mock.MockMessageHandler;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@@ -83,6 +89,9 @@ public class MockIntegrationContext implements BeanFactoryAware {
if (endpoint instanceof SourcePollingChannelAdapter) {
directFieldAccessor.setPropertyValue("source", e.getValue());
}
else if (endpoint instanceof IntegrationConsumer) {
directFieldAccessor.setPropertyValue("handler", e.getValue());
}
});
}
@@ -112,7 +121,47 @@ public class MockIntegrationContext implements BeanFactoryAware {
instead(pollingAdapterId, mockMessageSource, SourcePollingChannelAdapter.class, "source", autoStartup);
}
private void instead(String endpointId, Object mock, Class<?> endpointClass, String property,
public void instead(String consumerEndpointId, MessageHandler mockMessageHandler) {
instead(consumerEndpointId, mockMessageHandler, true);
}
public void instead(String consumerEndpointId, MessageHandler mockMessageHandler, boolean autoStartup) {
Object endpoint = this.beanFactory.getBean(consumerEndpointId, IntegrationConsumer.class);
if (autoStartup && endpoint instanceof Lifecycle) {
((Lifecycle) endpoint).stop();
}
DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(endpoint);
Object targetMessageHandler = directFieldAccessor.getPropertyValue("handler");
this.beans.put(consumerEndpointId, targetMessageHandler);
if (mockMessageHandler instanceof MessageProducer) {
if (targetMessageHandler instanceof MessageProducer) {
MessageChannel outputChannel = TestUtils.getPropertyValue(targetMessageHandler, "outputChannel",
MessageChannel.class);
((MessageProducer) mockMessageHandler).setOutputChannel(outputChannel);
}
else {
if (mockMessageHandler instanceof MockMessageHandler) {
if (TestUtils.getPropertyValue(mockMessageHandler, "hasReplies", Boolean.class)) {
throw new IllegalStateException("The [" + mockMessageHandler + "] " +
"with replies can't replace simple MessageHandler [" + targetMessageHandler + "]");
}
}
else {
throw new IllegalStateException("The MessageProducer handler [" + mockMessageHandler + "] " +
"can't replace simple MessageHandler [" + targetMessageHandler + "]");
}
}
}
directFieldAccessor.setPropertyValue("handler", mockMessageHandler);
if (autoStartup && endpoint instanceof Lifecycle) {
((Lifecycle) endpoint).start();
}
}
private void instead(String endpointId, Object messagingComponent, Class<?> endpointClass, String property,
boolean autoStartup) {
Object endpoint = this.beanFactory.getBean(endpointId, endpointClass);
if (autoStartup && endpoint instanceof Lifecycle) {
@@ -120,7 +169,7 @@ public class MockIntegrationContext implements BeanFactoryAware {
}
DirectFieldAccessor directFieldAccessor = new DirectFieldAccessor(endpoint);
this.beans.put(endpointId, directFieldAccessor.getPropertyValue(property));
directFieldAccessor.setPropertyValue("source", mock);
directFieldAccessor.setPropertyValue(property, messagingComponent);
if (autoStartup && endpoint instanceof Lifecycle) {
((Lifecycle) endpoint).start();
}

View File

@@ -16,11 +16,11 @@
package org.springframework.integration.test.mock;
import static org.mockito.BDDMockito.given;
import java.util.ArrayList;
import java.util.List;
import org.mockito.ArgumentCaptor;
import org.mockito.BDDMockito;
import org.mockito.Mockito;
import org.springframework.integration.core.MessageSource;
@@ -89,8 +89,8 @@ public final class MockIntegration {
public static MessageSource<?> mockMessageSource(Message<?> message) {
MessageSource messageSource = Mockito.mock(MessageSource.class);
given(messageSource.receive())
.<Message<?>>willReturn(message);
BDDMockito.given(messageSource.receive())
.willReturn(message);
return messageSource;
}
@@ -108,12 +108,29 @@ public final class MockIntegration {
public static MessageSource<?> mockMessageSource(Message<?> message, Message<?>... messages) {
MessageSource messageSource = Mockito.mock(MessageSource.class);
given(messageSource.receive())
BDDMockito.given(messageSource.receive())
.willReturn(message, messages);
return messageSource;
}
/**
* Build a {@link MockMessageHandler} instance.
* @return the {@link MockMessageHandler} instance ready for interaction
*/
public static MockMessageHandler mockMessageHandler() {
return mockMessageHandler(null);
}
/**
* Build a {@link MockMessageHandler} instance based on the provided {@link ArgumentCaptor}.
* @param messageArgumentCaptor the Mockito ArgumentCaptor to capture incoming messages
* @return the MockMessageHandler instance ready for interaction
*/
public static MockMessageHandler mockMessageHandler(ArgumentCaptor<Message<?>> messageArgumentCaptor) {
return new MockMessageHandler(messageArgumentCaptor);
}
private MockIntegration() {
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2017 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
*
* http://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.integration.test.mock;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.matchers.CapturingMatcher;
import org.springframework.integration.handler.AbstractMessageProducingHandler;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
/**
* The {@link AbstractMessageProducingHandler} extension for the mocking purpose in tests.
* <p>
* The provided {@link Consumer}s and {@link Function}s are applied to the incoming
* messages one at a time until the last, which is applied for all subsequent messages.
* The similar behavior exists in the
* {@code Mockito.doReturn(Object toBeReturned, Object... toBeReturnedNext)}.
* <p>
* Typically is used as a chain of stub actions:
* <pre class="code">
* {@code
* MockIntegration.mockMessageHandler()
* .handleNext(...)
* .handleNext(...)
* .handleNextAndReply(...)
* .handleNextAndReply(...)
* .handleNext(...)
* .handleNextAndReply(...);
* }
* </pre>
*
* @author Artem Bilan
*
* @since 5.0
*/
public class MockMessageHandler extends AbstractMessageProducingHandler {
protected final List<Function<Message<?>, ?>> messageFunctions = new LinkedList<>();
private final CapturingMatcher<Message<?>> capturingMatcher;
protected Function<Message<?>, ?> lastFunction;
protected boolean hasReplies;
@SuppressWarnings("unchecked")
protected MockMessageHandler(ArgumentCaptor<Message<?>> messageArgumentCaptor) {
if (messageArgumentCaptor != null) {
this.capturingMatcher = (CapturingMatcher<Message<?>>) TestUtils.getPropertyValue(messageArgumentCaptor,
"capturingMatcher", CapturingMatcher.class);
}
else {
this.capturingMatcher = null;
}
}
/**
* Add the {@link Consumer} to the stack to handle the next incoming message.
* @param nextMessageConsumer the Consumer to handle the next incoming message.
* @return this
*/
public MockMessageHandler handleNext(Consumer<Message<?>> nextMessageConsumer) {
this.lastFunction = m -> {
nextMessageConsumer.accept(m);
return null;
};
this.messageFunctions.add(this.lastFunction);
return this;
}
/**
* Add the {@link Function} to the stack to handle the next incoming message
* and produce reply for it.
* @param nextMessageFunction the Function to handle the next incoming message.
* @return this
*/
public MockMessageHandler handleNextAndReply(Function<Message<?>, ?> nextMessageFunction) {
this.lastFunction = nextMessageFunction;
this.messageFunctions.add(this.lastFunction);
this.hasReplies = true;
return this;
}
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
if (this.capturingMatcher != null) {
this.capturingMatcher.captureFrom(message);
}
Function<Message<?>, ?> function = this.lastFunction;
synchronized (this) {
Iterator<Function<Message<?>, ?>> iterator = this.messageFunctions.iterator();
if (iterator.hasNext()) {
function = iterator.next();
iterator.remove();
}
}
Object result = function.apply(message);
if (result != null) {
sendOutputs(result, message);
}
}
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright 2017 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
*
* http://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.integration.test.mock;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.integration.test.matcher.HeaderMatcher.hasHeader;
import static org.springframework.integration.test.matcher.PayloadAndHeaderMatcher.sameExceptIgnorableHeaders;
import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload;
import static org.springframework.integration.test.mock.MockIntegration.mockMessageHandler;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.handler.ExpressionEvaluatingMessageHandler;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.context.MockIntegrationContext;
import org.springframework.integration.test.context.SpringIntegrationTest;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author Artem Bilan
*
* @since 5.0
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MockMessageHandlerTests.Config.class)
@SpringIntegrationTest
public class MockMessageHandlerTests {
@Autowired
private ApplicationContext context;
@Autowired
private MockIntegrationContext mockIntegrationContext;
@Autowired
private MessageChannel mockHandlerChannel;
@Autowired
private MessageChannel pojoServiceChannel;
@Autowired
private MessageChannel rawChannel;
@Autowired
private QueueChannel results;
@Autowired
private ArgumentCaptor<Message<?>> messageArgumentCaptor;
@After
public void tearDown() {
this.mockIntegrationContext.resetBeans();
results.purge(null);
}
@Test
public void testMockMessageHandler() {
QueueChannel replies = new QueueChannel();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader("bar", "BAR")
.setHeader("baz", "BAZ")
.setReplyChannel(replies)
.build();
this.mockHandlerChannel.send(message);
this.mockHandlerChannel.send(message);
this.mockHandlerChannel.send(message);
Message<String> message1 = MessageBuilder.fromMessage(message)
.removeHeaders("bar", "baz")
.build();
this.mockHandlerChannel.send(message1);
for (int i = 0; i < 4; i++) {
Message<?> receive = replies.receive(10000);
assertNotNull(receive);
assertEquals("foo", receive.getPayload());
}
List<Message<?>> messages = this.messageArgumentCaptor.getAllValues();
assertEquals(4, messages.size());
assertThat(messages.get(0), hasHeader("bar", "BAR"));
assertThat(messages.get(1),
sameExceptIgnorableHeaders(MessageBuilder.withPayload("foo")
.setHeader("baz", "BAZ")
.build(),
"bar", MessageHeaders.REPLY_CHANNEL));
assertThat(messages.get(2), hasPayload("foo"));
assertThat(messages.get(3), hasPayload("foo"));
}
@Test
public void testMockMessageHandlerPojoService() {
this.pojoServiceChannel.send(new GenericMessage<>("bar"));
Message<?> receive = this.results.receive(10000);
assertNotNull(receive);
assertEquals("barbar", receive.getPayload());
MessageHandler mockMessageHandler =
mockMessageHandler()
.handleNextAndReply(m -> m.getPayload().toString().toUpperCase());
this.mockIntegrationContext.instead("mockMessageHandlerTests.Config.myService.serviceActivator",
mockMessageHandler);
this.pojoServiceChannel.send(new GenericMessage<>("foo"));
receive = this.results.receive(10000);
assertNotNull(receive);
assertEquals("FOO", receive.getPayload());
try {
this.pojoServiceChannel.send(new GenericMessage<>("bar"));
fail("AssertionError expected");
}
catch (Error e) {
assertThat(e, instanceOf(AssertionError.class));
}
}
@Test
@SuppressWarnings("unchecked")
public void testMockRawHandler() {
ArgumentCaptor<Message<?>> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class);
MessageHandler mockMessageHandler =
spy(mockMessageHandler(messageArgumentCaptor))
.handleNext(m -> { });
String endpointId = "rawHandlerConsumer";
this.mockIntegrationContext.instead(endpointId, mockMessageHandler);
Object endpoint = this.context.getBean(endpointId);
assertSame(mockMessageHandler, TestUtils.getPropertyValue(endpoint, "handler", MessageHandler.class));
GenericMessage<String> message = new GenericMessage<>("foo");
this.rawChannel.send(message);
verify(mockMessageHandler)
.handleMessage(message);
assertSame(message, messageArgumentCaptor.getValue());
this.mockIntegrationContext.resetBeans(endpointId);
mockMessageHandler =
mockMessageHandler()
.handleNextAndReply(m -> m);
try {
this.mockIntegrationContext.instead(endpointId, mockMessageHandler);
fail("IllegalStateException expected");
}
catch (Exception e) {
assertThat(e, instanceOf(IllegalStateException.class));
assertThat(e.getMessage(), containsString("with replies can't replace simple MessageHandler"));
}
}
@Configuration
@EnableIntegration
public static class Config {
@Bean
public PollableChannel results() {
return new QueueChannel();
}
@Bean
@SuppressWarnings("unchecked")
public ArgumentCaptor<Message<?>> messageArgumentCaptor() {
return ArgumentCaptor.forClass(Message.class);
}
@Bean
public PollableChannel mockHandlerChannel() {
return new QueueChannel();
}
@Bean
@ServiceActivator(inputChannel = "mockHandlerChannel",
poller = @Poller(fixedDelay = "100"))
public MessageHandler mockHandler() {
return mockMessageHandler(messageArgumentCaptor())
.handleNextAndReply(m -> m)
.handleNextAndReply(m -> m)
.handleNextAndReply(m -> "foo");
}
@ServiceActivator(inputChannel = "pojoServiceChannel", outputChannel = "results")
public String myService(String payload) {
return payload + payload;
}
@Bean
public SubscribableChannel rawChannel() {
return new DirectChannel();
}
@Bean
public EventDrivenConsumer rawHandlerConsumer() {
return new EventDrivenConsumer(rawChannel(),
new ExpressionEvaluatingMessageHandler(new ValueExpression<>("test")));
}
}
}