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

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-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.
@@ -16,11 +16,10 @@
package org.springframework.integration.test.matcher;
import static org.hamcrest.CoreMatchers.is;
import java.util.Date;
import java.util.Map;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
@@ -68,6 +67,7 @@ import org.springframework.messaging.MessageHeaders;
*
* @author Alex Peters
* @author Iwein Fuld
* @author Artem Bilan
*
*/
public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
@@ -75,27 +75,22 @@ public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
private final Matcher<?> matcher;
/**
* @param matcher
* @param matcher the target matcher to delegate
*/
HeaderMatcher(Matcher<?> matcher) {
private HeaderMatcher(Matcher<?> matcher) {
super();
this.matcher = matcher;
}
/**
* {@inheritDoc}
*/
@Override
public boolean matchesSafely(Message<?> item) {
return matcher.matches(item.getHeaders());
return this.matcher.matches(item.getHeaders());
}
/**
* {@inheritDoc}
*/
@Override
public void describeTo(Description description) {
description.appendText("a Message with Headers containing ").appendDescriptionOf(matcher);
description.appendText("a Message with Headers containing ")
.appendDescriptionOf(this.matcher);
}
@Factory
@@ -104,12 +99,12 @@ public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
}
@Factory
public static <T> Matcher<Message<?>> hasHeader(String key, Matcher<?> valueMatcher) {
public static <T> Matcher<Message<?>> hasHeader(String key, Matcher<T> valueMatcher) {
return new HeaderMatcher(MapContentMatchers.hasEntry(key, valueMatcher));
}
@Factory
public static <T> Matcher<Message<?>> hasHeaderKey(String key) {
public static Matcher<Message<?>> hasHeaderKey(String key) {
return new HeaderMatcher(MapContentMatchers.hasKey(key));
}
@@ -130,7 +125,7 @@ public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
@Factory
public static Matcher<Message<?>> hasSequenceNumber(Integer value) {
return hasSequenceNumber(is(value));
return hasSequenceNumber(CoreMatchers.is(value));
}
@Factory
@@ -140,7 +135,7 @@ public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
@Factory
public static Matcher<Message<?>> hasSequenceSize(Integer value) {
return hasSequenceSize(is(value));
return hasSequenceSize(CoreMatchers.is(value));
}
@Factory
@@ -150,7 +145,7 @@ public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
@Factory
public static Matcher<Message<?>> hasExpirationDate(Date value) {
return hasExpirationDate(is(value.getTime()));
return hasExpirationDate(CoreMatchers.is(value.getTime()));
}
@Factory
@@ -160,7 +155,7 @@ public class HeaderMatcher extends TypeSafeMatcher<Message<?>> {
@Factory
public static Matcher<Message<?>> hasTimestamp(Date value) {
return hasTimestamp(is(value.getTime()));
return hasTimestamp(CoreMatchers.is(value.getTime()));
}
@Factory

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-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.
@@ -16,8 +16,6 @@
package org.springframework.integration.test.matcher;
import static org.hamcrest.CoreMatchers.anything;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -25,8 +23,8 @@ import java.util.Map;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.core.AllOf;
import org.hamcrest.core.IsEqual;
/**
* Matchers that examine the contents of a {@link Map}.
@@ -62,6 +60,7 @@ import org.hamcrest.core.IsEqual;
* @author Alex Peters
* @author Iwein Fuld
* @author Gunnar Hillert
* @author Artem Bilan
*
*/
public class MapContentMatchers<T, V> extends
@@ -71,67 +70,48 @@ public class MapContentMatchers<T, V> extends
private final Matcher<V> valueMatcher;
/**
* @param key
* @param value
*/
MapContentMatchers(T key, V value) {
this(key, IsEqual.equalTo(value));
private MapContentMatchers(T key, V value) {
this(key, Matchers.equalTo(value));
}
/**
* @param key
* @param valueMatcher
*/
MapContentMatchers(T key, Matcher<V> valueMatcher) {
super();
private MapContentMatchers(T key, Matcher<V> valueMatcher) {
this.key = key;
this.valueMatcher = valueMatcher;
}
/**
* {@inheritDoc}
*/
@Override
public boolean matchesSafely(Map<? super T, ? super V> item) {
return item.containsKey(key) && valueMatcher.matches(item.get(key));
}
/**
* {@inheritDoc}
*/
@Override
public void describeTo(Description description) {
description.appendText("an entry with key ").appendValue(key)
.appendText(" and value matching ").appendDescriptionOf(
valueMatcher);
valueMatcher);
}
@Factory
public static <T, V> Matcher<Map<? super T, ? super V>> hasEntry(T key,
V value) {
return new MapContentMatchers<T, V>(key, value);
public static <T, V> Matcher<Map<? super T, ? super V>> hasEntry(T key, V value) {
return new MapContentMatchers<>(key, value);
}
@Factory
public static <T, V> Matcher<Map<? super T, ? super V>> hasEntry(T key,
Matcher<V> valueMatcher) {
return new MapContentMatchers<T, V>(key, valueMatcher);
public static <T, V> Matcher<Map<? super T, ? super V>> hasEntry(T key, Matcher<V> valueMatcher) {
return new MapContentMatchers<>(key, valueMatcher);
}
@Factory
@SuppressWarnings("unchecked")
public static <T, V> Matcher<Map<? super T, ? super V>> hasKey(T key) {
return new MapContentMatchers<T, V>(key, (Matcher<V>) anything("any Value"));
return new MapContentMatchers<>(key, (Matcher<V>) Matchers.anything());
}
@Factory
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <T, V> Matcher<Map<? super T, ? super V>> hasAllEntries(
Map<T, V> entries) {
List<Matcher<? extends Map<? super T, ? super V>>> matchers = new ArrayList<Matcher<? extends Map<? super T, ? super V>>>(
entries.size());
public static <T, V> Matcher<Map<? super T, ? super V>> hasAllEntries(Map<T, V> entries) {
List<Matcher<Map<? super T, ? super V>>> matchers = new ArrayList<>(entries.size());
for (Map.Entry<T, V> entry : entries.entrySet()) {
final V value = entry.getValue();
if (value instanceof Matcher<?>) {
@@ -144,4 +124,5 @@ public class MapContentMatchers<T, V> extends
//return AllOf.allOf(matchers); //Does not work with Hamcrest 1.3
return new AllOf(matchers);
}
}

View File

@@ -16,14 +16,11 @@
package org.springframework.integration.test.matcher;
import static org.mockito.ArgumentMatchers.argThat;
import static org.springframework.integration.test.matcher.HeaderMatcher.hasHeader;
import static org.springframework.integration.test.matcher.PayloadMatcher.hasPayload;
import java.util.Map;
import org.hamcrest.Matcher;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.internal.hamcrest.HamcrestArgumentMatcher;
@@ -75,30 +72,28 @@ public class MockitoMessageMatchers {
super();
}
@SuppressWarnings("unchecked")
public static <T> Message<T> messageWithPayload(Matcher<T> payloadMatcher) {
return argThat(new HamcrestArgumentMatcher<>(hasPayload(payloadMatcher)));
public static <T> Message<?> messageWithPayload(Matcher<? super T> payloadMatcher) {
return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(PayloadMatcher.hasPayload(payloadMatcher)));
}
@SuppressWarnings("unchecked")
public static <T> Message<T> messageWithPayload(T payload) {
return argThat(new HamcrestArgumentMatcher<>(hasPayload(payload)));
public static <T> Message<?> messageWithPayload(T payload) {
return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(PayloadMatcher.hasPayload(payload)));
}
public static Message<?> messageWithHeaderEntry(String key, Object value) {
return argThat(new HamcrestArgumentMatcher<>(hasHeader(key, value)));
return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasHeader(key, value)));
}
public static Message<?> messageWithHeaderKey(String key) {
return argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasHeaderKey(key)));
return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasHeaderKey(key)));
}
public static <T> Message<?> messageWithHeaderEntry(String key, Matcher<T> valueMatcher) {
return argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.<T>hasHeader(key, valueMatcher)));
return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.<T>hasHeader(key, valueMatcher)));
}
public static Message<?> messageWithHeaderEntries(Map<String, ?> entries) {
return argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasAllHeaders(entries)));
return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasAllHeaders(entries)));
}
}

View File

@@ -47,6 +47,7 @@ import org.springframework.messaging.MessageHeaders;
* </pre>
*
* @author Dave Syer
* @author Artem Bilan
*
*/
public class PayloadAndHeaderMatcher extends BaseMatcher<Message<?>> {
@@ -65,28 +66,32 @@ public class PayloadAndHeaderMatcher extends BaseMatcher<Message<?>> {
private PayloadAndHeaderMatcher(Message<?> expected, String... ignoreKeys) {
this.ignoreKeys = ignoreKeys;
this.payload = expected.getPayload();
this.headers = getHeaders(expected);
this.headers = extractHeadersToAssert(expected);
}
private Map<String, Object> getHeaders(Message<?> operand) {
HashMap<String, Object> headers = new HashMap<String, Object>(operand.getHeaders());
private Map<String, Object> extractHeadersToAssert(Message<?> operand) {
HashMap<String, Object> headers = new HashMap<>(operand.getHeaders());
headers.remove(MessageHeaders.ID);
headers.remove(MessageHeaders.TIMESTAMP);
for (String key : ignoreKeys) {
headers.remove(key);
if (this.ignoreKeys != null) {
for (String key : this.ignoreKeys) {
headers.remove(key);
}
}
return headers;
}
public boolean matches(Object arg) {
Message<?> input = (Message<?>) arg;
Map<String, Object> inputHeaders = getHeaders(input);
return input.getPayload().equals(payload) && inputHeaders.equals(headers);
Map<String, Object> inputHeaders = extractHeadersToAssert(input);
return input.getPayload().equals(this.payload) && inputHeaders.equals(this.headers);
}
public void describeTo(Description description) {
description.appendText("a Message with Headers that match except ID and timestamp for payload: ").appendValue(
payload).appendText(" and headers: ").appendValue(headers);
description.appendText("a Message with Headers that match except ID and timestamp for payload: ")
.appendValue(this.payload)
.appendText(" and headers: ")
.appendValue(this.headers);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-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.
@@ -56,44 +56,38 @@ import org.springframework.messaging.Message;
* @author Iwein Fuld
*
*/
@SuppressWarnings("rawtypes")
public class PayloadMatcher extends TypeSafeMatcher<Message> {
public class PayloadMatcher extends TypeSafeMatcher<Message<?>> {
private final Matcher matcher;
private final Matcher<?> matcher;
/**
* Create a PayloadMatcher that matches the payload of messages against the given matcher
*/
PayloadMatcher(Matcher matcher) {
private PayloadMatcher(Matcher<?> matcher) {
super();
this.matcher = matcher;
}
/**
* {@inheritDoc}
*/
@Override
public boolean matchesSafely(Message message) {
return matcher.matches(message.getPayload());
public boolean matchesSafely(Message<?> message) {
return this.matcher.matches(message.getPayload());
}
/**
* {@inheritDoc}
*/
//@Override
@Override
public void describeTo(Description description) {
description.appendText("a Message with payload: ").appendDescriptionOf(matcher);
description.appendText("a Message with payload: ")
.appendDescriptionOf(this.matcher);
}
@Factory
public static <T> Matcher<Message> hasPayload(T payload) {
public static <T> Matcher<Message<?>> hasPayload(T payload) {
return new PayloadMatcher(IsEqual.equalTo(payload));
}
@Factory
public static <T> Matcher<Message> hasPayload(Matcher<? super T> payloadMatcher) {
public static <T> Matcher<Message<?>> hasPayload(Matcher<? super T> payloadMatcher) {
return new PayloadMatcher(payloadMatcher);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-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.
@@ -16,11 +16,9 @@
package org.springframework.integration.test.support;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,8 +41,10 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
* <li>A payload or message to send as a request message on the inputChannel</li>
* <li>A handler to validate the response received on the outputChannel</li>
* </ul>
*
* @author David Turanski
* @author Gary Russell
* @author Artem Bilan
*/
@RunWith(SpringJUnit4ClassRunner.class)
public abstract class AbstractRequestResponseScenarioTests {
@@ -78,16 +78,16 @@ public abstract class AbstractRequestResponseScenarioTests {
((SubscribableChannel) outputChannel).subscribe(scenario.getResponseValidator());
}
assertTrue(name + ": message not sent on " + scenario.getInputChannelName(),
Assert.assertTrue(name + ": message not sent on " + scenario.getInputChannelName(),
inputChannel.send(scenario.getMessage()));
if (outputChannel instanceof PollableChannel) {
Message<?> response = ((PollableChannel) outputChannel).receive(10000);
assertNotNull(name + ": receive timeout on " + scenario.getOutputChannelName(), response);
Assert.assertNotNull(name + ": receive timeout on " + scenario.getOutputChannelName(), response);
scenario.getResponseValidator().handleMessage(response);
}
assertNotNull("message was not handled on " + outputChannel + " for scenario '" + name + "'.",
Assert.assertNotNull("message was not handled on " + outputChannel + " for scenario '" + name + "'.",
scenario.getResponseValidator().getLastMessage());
if (outputChannel instanceof SubscribableChannel) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-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.
@@ -35,51 +35,43 @@ import org.springframework.messaging.support.MessageBuilder;
* @author Alex Peters
* @author Iwein Fuld
* @author Gunnar Hillert
* @author Artem Bilan
*/
public class PayloadMatcherTests {
static final BigDecimal ANY_PAYLOAD = new BigDecimal("1.123");
private static final BigDecimal ANY_PAYLOAD = new BigDecimal("1.123");
Message<BigDecimal> message = MessageBuilder.withPayload(ANY_PAYLOAD).build();
private final Message<BigDecimal> message = MessageBuilder.withPayload(ANY_PAYLOAD).build();
@Test
public void hasPayload_withEqualValue_matches() throws Exception {
assertThat(message, hasPayload(new BigDecimal("1.123")));
assertThat(this.message, hasPayload(new BigDecimal("1.123")));
}
@Test
public void hasPayload_withNotEqualValue_notMatching() throws Exception {
assertThat(message, not(hasPayload(new BigDecimal("456"))));
assertThat(this.message, not(hasPayload(new BigDecimal("456"))));
}
@Test
public void hasPayload_withMatcher_matches() throws Exception {
assertThat(message,
hasPayload(is(instanceOf(BigDecimal.class))));
assertThat(message, hasPayload(notNullValue()));
assertThat(this.message, hasPayload(is(instanceOf(BigDecimal.class))));
assertThat(this.message, hasPayload(notNullValue()));
}
@Test
public void hasPayload_withNotMatchingMatcher_notMatching()
throws Exception {
assertThat(message, not((hasPayload(is(instanceOf(String.class))))));
public void hasPayload_withNotMatchingMatcher_notMatching() throws Exception {
assertThat(this.message, not((hasPayload(is(instanceOf(String.class))))));
}
@Test
public void readableException() throws Exception {
try {
assertThat(message, hasPayload("woot"));
assertThat(this.message, hasPayload("woot"));
}
catch (AssertionError ae) {
assertTrue(ae.getMessage().contains("Expected: a Message with payload: "));
}
}
@SuppressWarnings("rawtypes")
@Test
public void shouldMatchNonParametrizedMessage() throws Exception {
Message message = this.message;
assertThat(message, hasPayload(new BigDecimal("1.123")));
}
}

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")));
}
}
}

View File

@@ -96,6 +96,7 @@
org.springframework.integration.test.matcher.MockitoMessageMatchers.*,
org.springframework.integration.test.matcher.PayloadAndHeaderMatcher.*,
org.springframework.integration.test.matcher.PayloadMatcher.*,
org.springframework.integration.test.mock.MockIntegration.*,
org.springframework.integration.test.util.TestUtils.*,
org.springframework.test.web.client.match.MockRestRequestMatchers.*,
org.springframework.test.web.client.response.MockRestResponseCreators.*,

View File

@@ -287,6 +287,40 @@ assertNotNull(receive);
assertEquals("FOO", receive.getPayload());
----
Unlike the Mockito `MessageSource` mock object, the `MockMessageHandler` is just a regular `AbstractMessageProducingHandler` extension with a chain API to stub handling for incoming messages.
The `MockMessageHandler` provides `handleNext(Consumer<Message<?>>)` to specify a one-way stub for the next request message; used to mock message handlers that don't produce replies.
The `handleNextAndReply(Function<Message<?>, ?>)` is provided for performing the same stub logic for the next request message and producing a reply for it.
They can be chained to simulate any arbitrary request-reply scenarios for all expected request messages variants.
These consumers and functions are applied to the incoming messages, one at a time from the stack, until the last, which is then used for all remaining messages.
The behavior is similar to the Mockito `Answer` or `doReturn()` API.
In addition, a Mockito `ArgumentCaptor<Message<?>>` can be supplied to the `MockMessageHandler` in a constructor argument.
Each request message for the `MockMessageHandler` is captured by that `ArgumentCaptor`.
During the test, its `getValue()/getAllValues()` can be used to verify and assert those request messages.
The `MockIntegrationContext` provides an `instead()` API for replacing the actual configured `MessageHandler` with a `MockMessageHandler`, in the particular endpoint in the application context under test.
A typical usage might be:
[source,java]
----
ArgumentCaptor<Message<?>> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class);
MessageHandler mockMessageHandler =
mockMessageHandler(messageArgumentCaptor)
.handleNextAndReply(m -> m.getPayload().toString().toUpperCase());
this.mockIntegrationContext.instead("myService.serviceActivator", mockMessageHandler);
GenericMessage<String> message = new GenericMessage<>("foo");
this.myChannel.send(message);
Message<?> received = this.results.receive(10000);
assertNotNull(received);
assertEquals("FOO", received.getPayload());
assertSame(message, messageArgumentCaptor.getValue());
----
See `MockIntegration` and `MockMessageHandler` JavaDocs for more information.
[[testing-other-resources]]
=== Other Resources