ryoung10 ryoung10 -3 years ago 81
Java Question

Mockito ArgumentCaptor is capturing millions of repeated records

I'm trying to use Mockito and JUnit to test a multithreaded application. Here's some of the code in question:

ArgumentCaptor<MessageCreator> messageCaptor = ArgumentCaptor.forClass(MessageCreator.class);

jmsHandler.put(message);

Mockito.verify(mockJmsTemplate, Mockito.after(10000).times(4)).send(messageCaptor.capture());


The
jmsHandler.put(message)
line puts a
String
into the application, which goes onto a
BlockingQueue
and starts with the multithreading parts. I then wait on the method to execute 4 times over the next 10 seconds and capture the results.

The application should spit out 4 instances of
MessageCreator
, which, for my purposes, is just an object containing a
String
that I'll then compare to the expected output. The logging during the test confirms that the 4 messages are being created.

When I tried to loop through the
ArgumentCaptor
's
getAllValues()
method to check the results, though, I noticed that the
List
is duplicating my program's output literally millions of times. It should have 4 objects in it, but the last run had 6,984,988.

This number seems to be variable to some small extent, but it changes drastically when I play around with the test timings in debug mode. For example, if I set a breakpoint at the
jmsHandler
line, step over that, and wait for the application to finish processing before even starting the
Mockito.verify(...)
step, the
List
size plummets to a "mere" 158,636 objects.

Has anyone else run into this kind of issue before? Please let me know if I can provide any more details.

EDIT: Here's a self-contained example of the test and program structure:

import static org.junit.Assert.assertEquals;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;

public class MultiThreadTest {
private Input inputHandler;
@Mock
private JmsTemplate mockJmsTemplate;

@Before
public void setup() {
MockitoAnnotations.initMocks(this);

BlockingQueue<String> queue = new ArrayBlockingQueue<String>(10);

inputHandler = new Input();
inputHandler.setQueue(queue);

Output outputHandler = new Output();
outputHandler.setQueue(queue);
outputHandler.setJmsTemplate(mockJmsTemplate);
new Thread(outputHandler).start();
}

@Test
public void testMessage() {
ArgumentCaptor<OutputMessageCreator> messageCaptor = ArgumentCaptor.forClass(OutputMessageCreator.class);

String inMessage = "testMessage";

List<String> expectedMessages = new ArrayList<String>(4);

inputHandler.put(inMessage);

Mockito.verify(mockJmsTemplate, Mockito.after(10000).times(4)).send(messageCaptor.capture());
System.out.println("Number: " + messageCaptor.getAllValues().size());
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(0)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(1)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(2)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(3)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(4)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(5)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(6)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(7)));
System.out.println("Equal: " + messageCaptor.getAllValues().get(0).equals(messageCaptor.getAllValues().get(8)));

List<String> outMsgs = new ArrayList<String>();
for (OutputMessageCreator creator : messageCaptor.getAllValues()) {
outMsgs.add(creator.getMsg());
}
assertEquals(expectedMessages, outMsgs);
}

private class Input {
private BlockingQueue<String> queue;

public void put(String msg) {
try {
queue.put(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void setQueue(BlockingQueue<String> queue) {
this.queue = queue;
}
}

private class Output implements Runnable {
private BlockingQueue<String> queue;
private JmsTemplate jmsTemplate;
private int counter = 1;

@Override
public void run() {
while (true) {
String msg = null;
try {
msg = queue.take();
String[] messagesOut = new String[4];
for (int i = 0; i < 4; i++) {
messagesOut[i] = msg + "-" + counter++;
}

for (String messageOut : messagesOut) {
System.out.println(messageOut);
jmsTemplate.send(new OutputMessageCreator(messageOut));
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public void setQueue(BlockingQueue<String> queue) {
this.queue = queue;
}

public void setJmsTemplate(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
}

private class OutputMessageCreator implements MessageCreator {
private String msg;

public OutputMessageCreator(String msg) {
this.msg = msg;
}

@Override
public Message createMessage(Session session) throws JMSException {
TextMessage message = session.createTextMessage();
message.setText(msg);
return message;
}

public String getMsg() {
return msg;
}
}
}


Here's the output from running that test:

testMessage-1
testMessage-2
testMessage-3
testMessage-4
Number: 5392168
Equal: true
Equal: false
Equal: false
Equal: false
Equal: true
Equal: false
Equal: false
Equal: false
Equal: true

Answer Source

It looks like this was a bug in Mockito at some point:

https://github.com/mockito/mockito/issues/345

https://github.com/mockito/mockito/issues/379

The issues seem to imply this was fixed in Mockito 2 or 2.1, but I am using 2.0.41-beta and tried 2.4.0 and 2.8.9 and got the same results. In some of the comments of the github issues, it appears that there's a work around of sorts.

This line of the code:

Mockito.verify(mockJmsTemplate, Mockito.after(10000).times(4)).send(messageCaptor.capture());

can be replaced with this:

try {
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Mockito.verify(mockJmsTemplate, Mockito.times(4)).send(messageCaptor.capture());

I've commented on issue 379 to see if the fix was actually released, but it seems like this work around will do the trick for now.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download