Skip to content

Commit a141ede

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** # Conflicts: # spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java # spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java # spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/ExampleRabbitListenerSpyAndCaptureTest.java # spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/ExampleRabbitListenerSpyTest.java # spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java # src/reference/asciidoc/testing.adoc
1 parent 144bf81 commit a141ede

File tree

8 files changed

+167
-40
lines changed

8 files changed

+167
-40
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) {
@@ -79,6 +84,7 @@ protected void processListener(MethodRabbitListenerEndpoint endpoint, RabbitList
7984
String id = rabbitListener.id();
8085
if (StringUtils.hasText(id)) {
8186
if (this.attributes.getBoolean("spy")) {
87+
this.delegates.put(id, proxy);
8288
proxy = Mockito.mock(AopTestUtils.getUltimateTargetObject(proxy).getClass(),
8389
AdditionalAnswers.delegatesTo(proxy));
8490
this.listeners.put(id, proxy);
@@ -104,6 +110,31 @@ protected void processListener(MethodRabbitListenerEndpoint endpoint, RabbitList
104110
super.processListener(endpoint, rabbitListener, proxy, adminTarget, beanName);
105111
}
106112

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

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

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

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 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.
@@ -16,34 +16,67 @@
1616

1717
package org.springframework.amqp.rabbit.test.mockito;
1818

19+
import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations;
1920
import org.mockito.invocation.InvocationOnMock;
2021
import org.mockito.stubbing.Answer;
2122

23+
import org.springframework.lang.Nullable;
24+
2225
/**
2326
* An Answer to optionally call the real method and allow returning a
2427
* custom result.
2528
*
29+
* @param <T> the return type.
30+
*
2631
* @author Gary Russell
2732
* @since 1.6
2833
*
2934
*/
30-
public class LambdaAnswer<T> implements Answer<T> {
35+
@SuppressWarnings("serial")
36+
public class LambdaAnswer<T> extends ForwardsInvocations {
3137

3238
private final boolean callRealMethod;
3339

3440
private final ValueToReturn<T> callback;
3541

42+
private final boolean hasDelegate;
43+
44+
/**
45+
* Deprecated.
46+
* @param callRealMethod true to call the real method.
47+
* @param callback the callback.
48+
* @deprecated in favor of {@link #LambdaAnswer(boolean, ValueToReturn, Object)}.
49+
*/
50+
@Deprecated
3651
public LambdaAnswer(boolean callRealMethod, ValueToReturn<T> callback) {
52+
this(callRealMethod, callback, null);
53+
}
54+
55+
/**
56+
* Construct an instance with the provided properties. Use the test harness to get an
57+
* instance with the proper delegate.
58+
* @param callRealMethod true to call the real method.
59+
* @param callback the call back to receive the result.
60+
* @param delegate the delegate.
61+
*/
62+
public LambdaAnswer(boolean callRealMethod, ValueToReturn<T> callback, @Nullable Object delegate) {
63+
super(delegate);
3764
this.callRealMethod = callRealMethod;
3865
this.callback = callback;
66+
this.hasDelegate = delegate != null;
3967
}
4068

4169
@SuppressWarnings("unchecked")
4270
@Override
4371
public T answer(InvocationOnMock invocation) throws Throwable {
4472
T result = null;
4573
if (this.callRealMethod) {
46-
result = (T) invocation.callRealMethod();
74+
if (this.hasDelegate) {
75+
result = (T) super.answer(invocation);
76+
}
77+
else {
78+
result = (T) invocation.callRealMethod();
79+
}
4780
}
4881
return this.callback.apply(invocation, result);
4982
}

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

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016 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.
@@ -17,10 +17,14 @@
1717
package org.springframework.amqp.rabbit.test.mockito;
1818

1919
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.TimeUnit;
2021

22+
import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations;
2123
import org.mockito.invocation.InvocationOnMock;
2224
import org.mockito.stubbing.Answer;
2325

26+
import org.springframework.lang.Nullable;
27+
2428
/**
2529
* An Answer for void returning methods that calls the real method and
2630
* counts down a latch.
@@ -29,24 +33,63 @@
2933
* @since 1.6
3034
*
3135
*/
32-
public class LatchCountDownAndCallRealMethodAnswer implements Answer<Void> {
36+
@SuppressWarnings("serial")
37+
public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations {
3338

3439
private final CountDownLatch latch;
3540

41+
private final boolean hasDelegate;
42+
3643
/**
44+
* Get an instance with no delegate.
45+
* @deprecated in favor of
46+
* {@link #LatchCountDownAndCallRealMethodAnswer(int, Object)}.
3747
* @param count to set in a {@link CountDownLatch}.
3848
*/
49+
@Deprecated
3950
public LatchCountDownAndCallRealMethodAnswer(int count) {
51+
this(count, null);
52+
}
53+
54+
/**
55+
* Get an instance with the provided properties. Use the test harness to get an
56+
* instance with the proper delegate.
57+
* @param count the count.
58+
* @param delegate the delegate.
59+
* @since 2.1.16
60+
*/
61+
public LatchCountDownAndCallRealMethodAnswer(int count, @Nullable Object delegate) {
62+
super(delegate);
4063
this.latch = new CountDownLatch(count);
64+
this.hasDelegate = delegate != null;
4165
}
4266

4367
@Override
44-
public Void answer(InvocationOnMock invocation) throws Throwable {
45-
invocation.callRealMethod();
46-
this.latch.countDown();
68+
public Object answer(InvocationOnMock invocation) throws Throwable {
69+
try {
70+
if (this.hasDelegate) {
71+
return super.answer(invocation);
72+
}
73+
else {
74+
invocation.callRealMethod();
75+
}
76+
}
77+
finally {
78+
this.latch.countDown();
79+
}
4780
return null;
4881
}
4982

83+
/**
84+
* Wait for the latch to count down.
85+
* @param timeout the timeout in seconds.
86+
* @return the result of awaiting on the latch; true if counted down.
87+
* @throws InterruptedException if the thread is interrupted.
88+
* @since 2.1.16
89+
*/
90+
public boolean await(int timeout) throws InterruptedException {
91+
return this.latch.await(timeout, TimeUnit.SECONDS);
92+
}
5093

5194
public CountDownLatch getLatch() {
5295
return latch;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,14 @@ public void testOneWay() throws Exception {
9696
Listener listener = this.harness.getSpy("bar");
9797
assertNotNull(listener);
9898

99-
LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2);
99+
LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 3);
100100
doAnswer(answer).when(listener).foo(anyString(), anyString());
101101

102102
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
103103
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");
104104
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "ex");
105105

106-
assertTrue(answer.getLatch().await(10, TimeUnit.SECONDS));
106+
assertTrue(answer.await(10));
107107
verify(listener).foo("bar", this.queue2.getName());
108108
verify(listener).foo("baz", this.queue2.getName());
109109

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

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

26-
import java.util.concurrent.TimeUnit;
27-
2826
import org.junit.Rule;
2927
import org.junit.Test;
3028
import org.junit.runner.RunWith;
@@ -90,13 +88,13 @@ public void testOneWay() throws Exception {
9088
Listener listener = this.harness.getSpy("bar");
9189
assertNotNull(listener);
9290

93-
LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2);
91+
LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("bar", 2);
9492
doAnswer(answer).when(listener).foo(anyString(), anyString());
9593

9694
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar");
9795
this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz");
9896

99-
assertTrue(answer.getLatch().await(10, TimeUnit.SECONDS));
97+
assertTrue(answer.await(10));
10098
verify(listener).foo("bar", this.queue2.getName());
10199
verify(listener).foo("baz", this.queue2.getName());
102100
}

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

Lines changed: 17 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,12 +66,12 @@ 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");
7876
}
7977

@@ -84,7 +82,7 @@ public static class Config {
8482

8583
@Bean
8684
public Listener listener() {
87-
return (Listener) new ProxyFactory(new Listener()).getProxy();
85+
return (Listener) new ProxyFactory(new Listener(dependency())).getProxy();
8886
}
8987

9088
@Bean
@@ -113,12 +111,26 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Conne
113111
containerFactory.setConnectionFactory(cf);
114112
return containerFactory;
115113
}
114+
115+
@Bean
116+
public String dependency() {
117+
return "dependency";
118+
}
119+
116120
}
117121

118122
public static class Listener {
119123

124+
private final String dependency;
125+
126+
public Listener(String dependency) {
127+
this.dependency = dependency;
128+
}
129+
120130
@RabbitListener(id = "foo", queues = "#{queue.name}")
121131
public void foo(String foo) {
132+
this.dependency.substring(0);
122133
}
123134
}
135+
124136
}

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2017 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.
@@ -18,6 +18,7 @@
1818

1919
import static org.junit.Assert.assertEquals;
2020
import static org.mockito.ArgumentMatchers.anyString;
21+
import static org.mockito.BDDMockito.willAnswer;
2122
import static org.mockito.Mockito.doAnswer;
2223
import static org.mockito.Mockito.spy;
2324

@@ -32,14 +33,17 @@ public class AnswerTests {
3233

3334
@Test
3435
public void testLambda() {
35-
Foo foo = spy(new Foo());
36-
doAnswer(new LambdaAnswer<String>(true, (i, r) -> r + r)).when(foo).foo(anyString());
36+
Foo delegate = new Foo();
37+
Foo foo = spy(delegate);
38+
willAnswer(new LambdaAnswer<String>(true, (i, r) -> r + r, delegate)).given(foo).foo(anyString());
3739
assertEquals("FOOFOO", foo.foo("foo"));
38-
doAnswer(new LambdaAnswer<String>(true, (i, r) -> r + i.getArguments()[0])).when(foo).foo(anyString());
40+
willAnswer(new LambdaAnswer<String>(true, (i, r) -> r + i.getArguments()[0], delegate))
41+
.given(foo).foo(anyString());
3942
assertEquals("FOOfoo", foo.foo("foo"));
40-
doAnswer(new LambdaAnswer<String>(false, (i, r) ->
41-
"" + i.getArguments()[0] + i.getArguments()[0])).when(foo).foo(anyString());
43+
willAnswer(new LambdaAnswer<String>(false, (i, r) ->
44+
"" + i.getArguments()[0] + i.getArguments()[0], delegate)).given(foo).foo(anyString());
4245
assertEquals("foofoo", foo.foo("foo"));
46+
4347
}
4448

4549
private static class Foo {

0 commit comments

Comments
 (0)