From 079ccb84e21deabeb3867e97ed062ea243123458 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 8 May 2017 18:19:49 -0400 Subject: [PATCH] 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>) .handleNext(Consumer>) .handleNextAndReply(Function, Object>) .handleNext(Consumer>) .handleNextAndReply(Function, Object>) .handleNextAndReply(Function, Object>); ``` Doc Polishing --- .../test/matcher/HeaderMatcher.java | 33 +-- .../test/matcher/MapContentMatchers.java | 49 +--- .../test/matcher/MockitoMessageMatchers.java | 23 +- .../test/matcher/PayloadAndHeaderMatcher.java | 23 +- .../test/matcher/PayloadMatcher.java | 28 +- .../AbstractRequestResponseScenarioTests.java | 14 +- .../test/matcher/PayloadMatcherTests.java | 30 +- .../test/context/MockIntegrationContext.java | 53 +++- .../test/mock/MockIntegration.java | 27 +- .../test/mock/MockMessageHandler.java | 128 +++++++++ .../test/mock/MockMessageHandlerTests.java | 260 ++++++++++++++++++ src/checkstyle/checkstyle.xml | 1 + src/reference/asciidoc/testing.adoc | 34 +++ 13 files changed, 577 insertions(+), 126 deletions(-) create mode 100644 spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockMessageHandler.java create mode 100644 spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageHandlerTests.java diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java index 24344c5d37..217e09832b 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/HeaderMatcher.java @@ -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> { @@ -75,27 +75,22 @@ public class HeaderMatcher extends TypeSafeMatcher> { 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> { } @Factory - public static Matcher> hasHeader(String key, Matcher valueMatcher) { + public static Matcher> hasHeader(String key, Matcher valueMatcher) { return new HeaderMatcher(MapContentMatchers.hasEntry(key, valueMatcher)); } @Factory - public static Matcher> hasHeaderKey(String key) { + public static Matcher> hasHeaderKey(String key) { return new HeaderMatcher(MapContentMatchers.hasKey(key)); } @@ -130,7 +125,7 @@ public class HeaderMatcher extends TypeSafeMatcher> { @Factory public static Matcher> hasSequenceNumber(Integer value) { - return hasSequenceNumber(is(value)); + return hasSequenceNumber(CoreMatchers.is(value)); } @Factory @@ -140,7 +135,7 @@ public class HeaderMatcher extends TypeSafeMatcher> { @Factory public static Matcher> hasSequenceSize(Integer value) { - return hasSequenceSize(is(value)); + return hasSequenceSize(CoreMatchers.is(value)); } @Factory @@ -150,7 +145,7 @@ public class HeaderMatcher extends TypeSafeMatcher> { @Factory public static Matcher> hasExpirationDate(Date value) { - return hasExpirationDate(is(value.getTime())); + return hasExpirationDate(CoreMatchers.is(value.getTime())); } @Factory @@ -160,7 +155,7 @@ public class HeaderMatcher extends TypeSafeMatcher> { @Factory public static Matcher> hasTimestamp(Date value) { - return hasTimestamp(is(value.getTime())); + return hasTimestamp(CoreMatchers.is(value.getTime())); } @Factory diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java index cfc6b1161b..144abf3432 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MapContentMatchers.java @@ -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 extends @@ -71,67 +70,48 @@ public class MapContentMatchers extends private final Matcher 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 valueMatcher) { - super(); + private MapContentMatchers(T key, Matcher valueMatcher) { this.key = key; this.valueMatcher = valueMatcher; } - /** - * {@inheritDoc} - */ @Override public boolean matchesSafely(Map 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 Matcher> hasEntry(T key, - V value) { - return new MapContentMatchers(key, value); + public static Matcher> hasEntry(T key, V value) { + return new MapContentMatchers<>(key, value); } @Factory - public static Matcher> hasEntry(T key, - Matcher valueMatcher) { - return new MapContentMatchers(key, valueMatcher); + public static Matcher> hasEntry(T key, Matcher valueMatcher) { + return new MapContentMatchers<>(key, valueMatcher); } @Factory @SuppressWarnings("unchecked") public static Matcher> hasKey(T key) { - return new MapContentMatchers(key, (Matcher) anything("any Value")); + return new MapContentMatchers<>(key, (Matcher) Matchers.anything()); } @Factory @SuppressWarnings({ "unchecked", "rawtypes" }) - public static Matcher> hasAllEntries( - Map entries) { - List>> matchers = new ArrayList>>( - entries.size()); + public static Matcher> hasAllEntries(Map entries) { + List>> matchers = new ArrayList<>(entries.size()); for (Map.Entry entry : entries.entrySet()) { final V value = entry.getValue(); if (value instanceof Matcher) { @@ -144,4 +124,5 @@ public class MapContentMatchers extends //return AllOf.allOf(matchers); //Does not work with Hamcrest 1.3 return new AllOf(matchers); } + } diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java index da832ddbba..21d6dd3cf4 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/MockitoMessageMatchers.java @@ -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 Message messageWithPayload(Matcher payloadMatcher) { - return argThat(new HamcrestArgumentMatcher<>(hasPayload(payloadMatcher))); + public static Message messageWithPayload(Matcher payloadMatcher) { + return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(PayloadMatcher.hasPayload(payloadMatcher))); } - @SuppressWarnings("unchecked") - public static Message messageWithPayload(T payload) { - return argThat(new HamcrestArgumentMatcher<>(hasPayload(payload))); + public static 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 Message messageWithHeaderEntry(String key, Matcher valueMatcher) { - return argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasHeader(key, valueMatcher))); + return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasHeader(key, valueMatcher))); } public static Message messageWithHeaderEntries(Map entries) { - return argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasAllHeaders(entries))); + return ArgumentMatchers.argThat(new HamcrestArgumentMatcher<>(HeaderMatcher.hasAllHeaders(entries))); } } diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java index 4256ecedd3..e81cc7ff79 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadAndHeaderMatcher.java @@ -47,6 +47,7 @@ import org.springframework.messaging.MessageHeaders; * * * @author Dave Syer + * @author Artem Bilan * */ public class PayloadAndHeaderMatcher extends BaseMatcher> { @@ -65,28 +66,32 @@ public class PayloadAndHeaderMatcher extends BaseMatcher> { private PayloadAndHeaderMatcher(Message expected, String... ignoreKeys) { this.ignoreKeys = ignoreKeys; this.payload = expected.getPayload(); - this.headers = getHeaders(expected); + this.headers = extractHeadersToAssert(expected); } - private Map getHeaders(Message operand) { - HashMap headers = new HashMap(operand.getHeaders()); + private Map extractHeadersToAssert(Message operand) { + HashMap 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 inputHeaders = getHeaders(input); - return input.getPayload().equals(payload) && inputHeaders.equals(headers); + Map 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); } } diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java index c03efb9531..de36257dda 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/matcher/PayloadMatcher.java @@ -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 { +public class PayloadMatcher extends TypeSafeMatcher> { - 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 Matcher hasPayload(T payload) { + public static Matcher> hasPayload(T payload) { return new PayloadMatcher(IsEqual.equalTo(payload)); } @Factory - public static Matcher hasPayload(Matcher payloadMatcher) { + public static Matcher> hasPayload(Matcher payloadMatcher) { return new PayloadMatcher(payloadMatcher); } + } diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java index 01eb7cec69..c7311bf1ab 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/support/AbstractRequestResponseScenarioTests.java @@ -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; *
  • A payload or message to send as a request message on the inputChannel
  • *
  • A handler to validate the response received on the outputChannel
  • * + * * @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) { diff --git a/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java b/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java index 98089497ae..b5436cda0c 100644 --- a/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java +++ b/spring-integration-test-support/src/test/java/org/springframework/integration/test/matcher/PayloadMatcherTests.java @@ -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 message = MessageBuilder.withPayload(ANY_PAYLOAD).build(); + private final Message 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"))); - } - } diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java index fa13bb4cf9..8c17f091d5 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/context/MockIntegrationContext.java @@ -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(); } diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java index 687db51eb2..64bdd23f0c 100644 --- a/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockIntegration.java @@ -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()) - .>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> messageArgumentCaptor) { + return new MockMessageHandler(messageArgumentCaptor); + } + private MockIntegration() { } diff --git a/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockMessageHandler.java b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockMessageHandler.java new file mode 100644 index 0000000000..24c7e132c0 --- /dev/null +++ b/spring-integration-test/src/main/java/org/springframework/integration/test/mock/MockMessageHandler.java @@ -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. + *

    + * 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)}. + *

    + * Typically is used as a chain of stub actions: + *

    + * {@code
    + *      MockIntegration.mockMessageHandler()
    + *               .handleNext(...)
    + *               .handleNext(...)
    + *               .handleNextAndReply(...)
    + *               .handleNextAndReply(...)
    + *               .handleNext(...)
    + *               .handleNextAndReply(...);
    + * }
    + * 
    + * + * @author Artem Bilan + * + * @since 5.0 + */ +public class MockMessageHandler extends AbstractMessageProducingHandler { + + protected final List, ?>> messageFunctions = new LinkedList<>(); + + private final CapturingMatcher> capturingMatcher; + + protected Function, ?> lastFunction; + + protected boolean hasReplies; + + @SuppressWarnings("unchecked") + protected MockMessageHandler(ArgumentCaptor> messageArgumentCaptor) { + if (messageArgumentCaptor != null) { + this.capturingMatcher = (CapturingMatcher>) 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> 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, ?> 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, ?> function = this.lastFunction; + + synchronized (this) { + Iterator, ?>> iterator = this.messageFunctions.iterator(); + if (iterator.hasNext()) { + function = iterator.next(); + iterator.remove(); + } + } + + Object result = function.apply(message); + + if (result != null) { + sendOutputs(result, message); + } + } + +} diff --git a/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageHandlerTests.java b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageHandlerTests.java new file mode 100644 index 0000000000..96f5de08a7 --- /dev/null +++ b/spring-integration-test/src/test/java/org/springframework/integration/test/mock/MockMessageHandlerTests.java @@ -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> messageArgumentCaptor; + + @After + public void tearDown() { + this.mockIntegrationContext.resetBeans(); + results.purge(null); + } + + @Test + public void testMockMessageHandler() { + QueueChannel replies = new QueueChannel(); + Message 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 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> 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> 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 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> 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"))); + } + + } + +} diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index f592dc11b3..ce994a091c 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -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.*, diff --git a/src/reference/asciidoc/testing.adoc b/src/reference/asciidoc/testing.adoc index 4727898beb..a06b67a56f 100644 --- a/src/reference/asciidoc/testing.adoc +++ b/src/reference/asciidoc/testing.adoc @@ -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>)` to specify a one-way stub for the next request message; used to mock message handlers that don't produce replies. +The `handleNextAndReply(Function, ?>)` 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>` 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> messageArgumentCaptor = ArgumentCaptor.forClass(Message.class); + +MessageHandler mockMessageHandler = + mockMessageHandler(messageArgumentCaptor) + .handleNextAndReply(m -> m.getPayload().toString().toUpperCase()); + +this.mockIntegrationContext.instead("myService.serviceActivator", mockMessageHandler); +GenericMessage 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