Skip to content

Commit e34783d

Browse files
garyrussellartembilan
authored andcommitted
GH-1226: Fix Test Harness
Resolves #1226 #1157 changed the way we spy listeners - to support CGLIB proxies (e.g. `@Transactional`). Instead of spying the listener, it mocks the listener and sets a default answer to call the real method on the delegate. This broke when users used other answers, such as those provided by the framework. Change the provided answers to subclass `ForwardsInvocation`. Also fix `ConcurrentModificationException` in `getExceptions()`. **cherry-pick to 2.2.x, 2.1.x**
1 parent 3b5d225 commit e34783d

File tree

8 files changed

+165
-30
lines changed

8 files changed

+165
-30
lines changed

spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
import org.springframework.amqp.rabbit.annotation.RabbitListener;
3535
import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor;
3636
import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint;
37+
import org.springframework.amqp.rabbit.test.mockito.LambdaAnswer;
38+
import org.springframework.amqp.rabbit.test.mockito.LambdaAnswer.ValueToReturn;
39+
import org.springframework.amqp.rabbit.test.mockito.LatchCountDownAndCallRealMethodAnswer;
3740
import org.springframework.aop.framework.ProxyFactoryBean;
3841
import org.springframework.core.annotation.AnnotationAttributes;
3942
import org.springframework.core.annotation.AnnotationUtils;
@@ -63,6 +66,8 @@ public class RabbitListenerTestHarness extends RabbitListenerAnnotationBeanPostP
6366

6467
private final Map<String, Object> listeners = new HashMap<>();
6568

69+
private final Map<String, Object> delegates = new HashMap<>();
70+
6671
private final AnnotationAttributes attributes;
6772

6873
public RabbitListenerTestHarness(AnnotationMetadata importMetadata) {
@@ -80,6 +85,7 @@ protected void processListener(MethodRabbitListenerEndpoint endpoint, RabbitList
8085
String id = rabbitListener.id();
8186
if (StringUtils.hasText(id)) {
8287
if (this.attributes.getBoolean("spy")) {
88+
this.delegates.put(id, proxy);
8389
proxy = Mockito.mock(AopTestUtils.getUltimateTargetObject(proxy).getClass(),
8490
AdditionalAnswers.delegatesTo(proxy));
8591
this.listeners.put(id, proxy);
@@ -105,6 +111,31 @@ protected void processListener(MethodRabbitListenerEndpoint endpoint, RabbitList
105111
super.processListener(endpoint, rabbitListener, proxy, target, beanName);
106112
}
107113

114+
/**
115+
* Return a {@link LatchCountDownAndCallRealMethodAnswer} that is properly configured
116+
* to invoke the listener.
117+
* @param id the listener id.
118+
* @param count the count.
119+
* @return the answer.
120+
* @since 2.1.16
121+
*/
122+
public LatchCountDownAndCallRealMethodAnswer getLatchAnswerFor(String id, int count) {
123+
return new LatchCountDownAndCallRealMethodAnswer(count, this.delegates.get(id));
124+
}
125+
126+
/**
127+
* Return a {@link LambdaAnswer} that is properly configured to invoke the listener.
128+
* @param <T> the return type.
129+
* @param id the listener id.
130+
* @param callRealMethod true to call the real method.
131+
* @param callback the callback.
132+
* @return the answer.
133+
* @since 2.1.16
134+
*/
135+
public <T> LambdaAnswer<T> getLambdaAnswerFor(String id, boolean callRealMethod, ValueToReturn<T> callback) {
136+
return new LambdaAnswer<>(callRealMethod, callback, this.delegates.get(id));
137+
}
138+
108139
public InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit unit) throws InterruptedException {
109140
CaptureAdvice advice = this.listenerCapture.get(id);
110141
if (advice != null) {
@@ -118,6 +149,18 @@ public <T> T getSpy(String id) {
118149
return (T) this.listeners.get(id);
119150
}
120151

152+
/**
153+
* Get the actual listener object (not the spy).
154+
* @param <T> the type.
155+
* @param id the id.
156+
* @return the listener.
157+
* @since 2.1.16
158+
*/
159+
@SuppressWarnings("unchecked")
160+
public <T> T getDelegate(String id) {
161+
return (T) this.delegates.get(id);
162+
}
163+
121164
private static final class CaptureAdvice implements MethodInterceptor {
122165

123166
private final BlockingQueue<InvocationData> invocationData = new LinkedBlockingQueue<>();

spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2019 the original author or authors.
2+
* Copyright 2016-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,28 +21,56 @@
2121
import java.util.LinkedHashSet;
2222
import java.util.Set;
2323

24+
import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations;
2425
import org.mockito.invocation.InvocationOnMock;
2526
import org.mockito.stubbing.Answer;
2627

28+
import org.springframework.lang.Nullable;
29+
2730
/**
2831
* An {@link Answer} to optionally call the real method and allow returning a
2932
* custom result. Captures any exceptions thrown.
3033
*
34+
* @param <T> the return type.
35+
*
3136
* @author Gary Russell
3237
* @since 1.6
3338
*
3439
*/
35-
public class LambdaAnswer<T> implements Answer<T> {
40+
@SuppressWarnings("serial")
41+
public class LambdaAnswer<T> extends ForwardsInvocations {
3642

3743
private final boolean callRealMethod;
3844

3945
private final ValueToReturn<T> callback;
4046

4147
private final Set<Exception> exceptions = Collections.synchronizedSet(new LinkedHashSet<>());
4248

49+
private final boolean hasDelegate;
50+
51+
/**
52+
* Deprecated.
53+
* @param callRealMethod true to call the real method.
54+
* @param callback the callback.
55+
* @deprecated in favor of {@link #LambdaAnswer(boolean, ValueToReturn, Object)}.
56+
*/
57+
@Deprecated
4358
public LambdaAnswer(boolean callRealMethod, ValueToReturn<T> callback) {
59+
this(callRealMethod, callback, null);
60+
}
61+
62+
/**
63+
* Construct an instance with the provided properties. Use the test harness to get an
64+
* instance with the proper delegate.
65+
* @param callRealMethod true to call the real method.
66+
* @param callback the call back to receive the result.
67+
* @param delegate the delegate.
68+
*/
69+
public LambdaAnswer(boolean callRealMethod, ValueToReturn<T> callback, @Nullable Object delegate) {
70+
super(delegate);
4471
this.callRealMethod = callRealMethod;
4572
this.callback = callback;
73+
this.hasDelegate = delegate != null;
4674
}
4775

4876
@SuppressWarnings("unchecked")
@@ -51,7 +79,12 @@ public T answer(InvocationOnMock invocation) throws Throwable {
5179
T result = null;
5280
try {
5381
if (this.callRealMethod) {
54-
result = (T) invocation.callRealMethod();
82+
if (this.hasDelegate) {
83+
result = (T) super.answer(invocation);
84+
}
85+
else {
86+
result = (T) invocation.callRealMethod();
87+
}
5588
}
5689
return this.callback.apply(invocation, result);
5790
}
@@ -67,7 +100,9 @@ public T answer(InvocationOnMock invocation) throws Throwable {
67100
* @since 2.2.3
68101
*/
69102
public Collection<Exception> getExceptions() {
70-
return Collections.unmodifiableCollection(this.exceptions);
103+
synchronized (this.exceptions) {
104+
return new LinkedHashSet<>(this.exceptions);
105+
}
71106
}
72107

73108
@FunctionalInterface

spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2019 the original author or authors.
2+
* Copyright 2016-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,7 +21,9 @@
2121
import java.util.LinkedHashSet;
2222
import java.util.Set;
2323
import java.util.concurrent.CountDownLatch;
24+
import java.util.concurrent.TimeUnit;
2425

26+
import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations;
2527
import org.mockito.invocation.InvocationOnMock;
2628
import org.mockito.stubbing.Answer;
2729

@@ -35,23 +37,48 @@
3537
* @since 1.6
3638
*
3739
*/
38-
public class LatchCountDownAndCallRealMethodAnswer implements Answer<Void> {
40+
@SuppressWarnings("serial")
41+
public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations {
3942

4043
private final CountDownLatch latch;
4144

4245
private final Set<Exception> exceptions = Collections.synchronizedSet(new LinkedHashSet<>());
4346

47+
private final boolean hasDelegate;
48+
4449
/**
50+
* Get an instance with no delegate.
51+
* @deprecated in favor of
52+
* {@link #LatchCountDownAndCallRealMethodAnswer(int, Object)}.
4553
* @param count to set in a {@link CountDownLatch}.
4654
*/
55+
@Deprecated
4756
public LatchCountDownAndCallRealMethodAnswer(int count) {
57+
this(count, null);
58+
}
59+
60+
/**
61+
* Get an instance with the provided properties. Use the test harness to get an
62+
* instance with the proper delegate.
63+
* @param count the count.
64+
* @param delegate the delegate.
65+
* @since 2.1.16
66+
*/
67+
public LatchCountDownAndCallRealMethodAnswer(int count, @Nullable Object delegate) {
68+
super(delegate);
4869
this.latch = new CountDownLatch(count);
70+
this.hasDelegate = delegate != null;
4971
}
5072

5173
@Override
52-
public Void answer(InvocationOnMock invocation) throws Throwable {
74+
public Object answer(InvocationOnMock invocation) throws Throwable {
5375
try {
54-
invocation.callRealMethod();
76+
if (this.hasDelegate) {
77+
return super.answer(invocation);
78+
}
79+
else {
80+
invocation.callRealMethod();
81+
}
5582
}
5683
catch (Exception e) {
5784
this.exceptions.add(e);
@@ -63,6 +90,16 @@ public Void answer(InvocationOnMock invocation) throws Throwable {
6390
return null;
6491
}
6592

93+
/**
94+
* Wait for the latch to count down.
95+
* @param timeout the timeout in seconds.
96+
* @return the result of awaiting on the latch; true if counted down.
97+
* @throws InterruptedException if the thread is interrupted.
98+
* @since 2.1.16
99+
*/
100+
public boolean await(int timeout) throws InterruptedException {
101+
return this.latch.await(timeout, TimeUnit.SECONDS);
102+
}
66103

67104
public CountDownLatch getLatch() {
68105
return latch;
@@ -75,7 +112,9 @@ public CountDownLatch getLatch() {
75112
*/
76113
@Nullable
77114
public Collection<Exception> getExceptions() {
78-
return Collections.unmodifiableCollection(this.exceptions);
115+
synchronized (this.exceptions) {
116+
return new LinkedHashSet<>(this.exceptions);
117+
}
79118
}
80119

81120
}

spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import static org.mockito.Mockito.doAnswer;
2222
import static org.mockito.Mockito.verify;
2323

24-
import java.util.concurrent.TimeUnit;
25-
2624
import org.junit.jupiter.api.Test;
2725

2826
import org.springframework.amqp.core.AnonymousQueue;
@@ -68,13 +66,14 @@ public void testProxiedListenerSpy() throws Exception {
6866
Listener listener = this.harness.getSpy("foo");
6967
assertThat(listener).isNotNull();
7068

71-
LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(1);
69+
LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("foo", 1);
7270
doAnswer(answer).when(listener).foo(anyString());
7371

7472
this.rabbitTemplate.convertAndSend(this.queue.getName(), "foo");
7573

76-
assertThat(answer.getLatch().await(10, TimeUnit.SECONDS)).isTrue();
74+
assertThat(answer.await(10)).isTrue();
7775
verify(listener).foo("foo");
76+
assertThat(answer.getExceptions()).isEmpty();
7877
}
7978

8079
@Configuration
@@ -84,7 +83,7 @@ public static class Config {
8483

8584
@Bean
8685
public Listener listener() {
87-
return (Listener) new ProxyFactory(new Listener()).getProxy();
86+
return (Listener) new ProxyFactory(new Listener(dependency())).getProxy();
8887
}
8988

9089
@Bean
@@ -113,12 +112,26 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Conne
113112
containerFactory.setConnectionFactory(cf);
114113
return containerFactory;
115114
}
115+
116+
@Bean
117+
public String dependency() {
118+
return "dependency";
119+
}
120+
116121
}
117122

118123
public static class Listener {
119124

125+
private final String dependency;
126+
127+
public Listener(String dependency) {
128+
this.dependency = dependency;
129+
}
130+
120131
@RabbitListener(id = "foo", queues = "#{queue.name}")
121132
public void foo(String foo) {
133+
this.dependency.substring(0);
122134
}
123135
}
136+
124137
}

spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ public void testOneWay() throws Exception {
8888
Listener listener = this.harness.getSpy("bar");
8989
assertThat(listener).isNotNull();
9090

91-
LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2);
91+
LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 3);
9292
doAnswer(answer).when(listener).foo(anyString(), anyString());
9393

9494
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
9595
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");
9696
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "ex");
9797

98-
assertThat(answer.getLatch().await(10, TimeUnit.SECONDS)).isTrue();
98+
assertThat(answer.await(10)).isTrue();
9999
verify(listener).foo("bar", this.queue2.getName());
100100
verify(listener).foo("baz", this.queue2.getName());
101101

spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
import static org.mockito.Mockito.doAnswer;
2222
import static org.mockito.Mockito.verify;
2323

24-
import java.util.concurrent.TimeUnit;
25-
2624
import org.junit.jupiter.api.Test;
2725

2826
import org.springframework.amqp.core.AnonymousQueue;
@@ -84,13 +82,13 @@ public void testOneWay() throws Exception {
8482
Listener listener = this.harness.getSpy("bar");
8583
assertThat(listener).isNotNull();
8684

87-
LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2);
85+
LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 2);
8886
doAnswer(answer).when(listener).foo(anyString(), anyString());
8987

9088
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
9189
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");
9290

93-
assertThat(answer.getLatch().await(10, TimeUnit.SECONDS)).isTrue();
91+
assertThat(answer.await(10)).isTrue();
9492
verify(listener).foo("bar", this.queue2.getName());
9593
verify(listener).foo("baz", this.queue2.getName());
9694
}

spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2019 the original author or authors.
2+
* Copyright 2016-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,15 +35,17 @@ public class AnswerTests {
3535

3636
@Test
3737
public void testLambda() {
38-
Foo foo = spy(new Foo());
39-
willAnswer(new LambdaAnswer<String>(true, (i, r) -> r + r)).given(foo).foo(anyString());
38+
Foo delegate = new Foo();
39+
Foo foo = spy(delegate);
40+
willAnswer(new LambdaAnswer<String>(true, (i, r) -> r + r, delegate)).given(foo).foo(anyString());
4041
assertThat(foo.foo("foo")).isEqualTo("FOOFOO");
41-
willAnswer(new LambdaAnswer<String>(true, (i, r) -> r + i.getArguments()[0])).given(foo).foo(anyString());
42+
willAnswer(new LambdaAnswer<String>(true, (i, r) -> r + i.getArguments()[0], delegate))
43+
.given(foo).foo(anyString());
4244
assertThat(foo.foo("foo")).isEqualTo("FOOfoo");
4345
willAnswer(new LambdaAnswer<String>(false, (i, r) ->
44-
"" + i.getArguments()[0] + i.getArguments()[0])).given(foo).foo(anyString());
46+
"" + i.getArguments()[0] + i.getArguments()[0], delegate)).given(foo).foo(anyString());
4547
assertThat(foo.foo("foo")).isEqualTo("foofoo");
46-
LambdaAnswer<String> answer = new LambdaAnswer<>(true, (inv, result) -> result);
48+
LambdaAnswer<String> answer = new LambdaAnswer<>(true, (inv, result) -> result, delegate);
4749
willAnswer(answer).given(foo).foo("fail");
4850
assertThatIllegalArgumentException().isThrownBy(() -> foo.foo("fail"));
4951
Collection<Exception> exceptions = answer.getExceptions();

0 commit comments

Comments
 (0)